diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.css b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.css index d1a782b36e..a5eaf49c60 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.css @@ -67,19 +67,28 @@ .research-progress .research-effect-citations button { display: inline-flex; align-items: center; gap: 4px; min-height: 44px; font: inherit; font-size: var(--text-body); background: transparent; border: 0; border-radius: 0; padding: 4px 0; color: var(--pw-muted); cursor: pointer; } .research-effect-citations button:hover { text-decoration: underline; } .research-native-results { border-block-start: 1px solid var(--pw-line); margin-block-start: 16px; padding-block-start: 16px; } -.research-native-results h4 { margin: 0; font-size: var(--text-caption); font-weight: 600; } +.research-native-results h4 { margin: 0; font-size: var(--text-body); font-weight: 600; } .research-native-results > p { margin-block: 8px; } -.research-native-values { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px 16px; margin-block: 12px; } -.research-native-values > div { min-width: 0; } -.research-native-values dt, .research-native-values dd > span { font-size: var(--text-caption); color: var(--pw-muted); } -.research-native-values dd { margin: 0; } -.research-native-values dd button { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0; font: inherit; background: none; border: 0; color: var(--pw-text); cursor: pointer; } -.research-native-values dd button:hover { text-decoration: underline; } -.research-native-values dd button:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 2px; } +.research-native-reading { display: flex; align-items: center; flex-wrap: wrap; gap: 16px 48px; margin-block: 16px; } +.research-native-best { min-width: 0; } +.research-native-reading dl { margin: 0; } +.research-native-reading dt { font-size: var(--text-body); color: var(--pw-muted); } +.research-native-reading dd { margin: 4px 0 0; } +.research-native-reading dd button { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 8px; max-width: 100%; min-height: 44px; padding: 0; font: inherit; background: none; border: 0; color: var(--pw-text); text-align: start; cursor: pointer; } +.research-native-best dd button { align-items: baseline; } +.research-native-reading dd button svg { align-self: center; flex-shrink: 0; color: var(--pw-muted); } +.research-native-reading dd button:hover strong { text-decoration: underline; } +.research-native-reading dd button:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 2px; } +.research-native-reading strong { font-weight: 600; font-variant-numeric: tabular-nums; font-size: var(--text-lead); } +.research-native-best strong { font-size: var(--text-display); line-height: var(--leading-title); } +.research-native-unit { font-size: var(--text-body); color: var(--pw-muted); } +.research-native-best > p { margin: 4px 0 0; font-size: var(--text-body); } +.research-native-context { display: flex; align-items: baseline; flex-wrap: wrap; gap: 12px 32px; } +.research-native-context > div { min-width: 0; } +.research-native-unknown { display: inline-block; padding-block: 10px; font-size: var(--text-body); color: var(--pw-muted); } +.research-native-scope { display: flex; flex-wrap: wrap; gap: 4px 16px; color: var(--pw-muted); font-size: var(--text-body); } .research-native-basis dd { margin: 4px 0 12px; } -.research-native-values strong { font-size: var(--text-body); font-weight: 600; font-variant-numeric: tabular-nums; } -.research-native-values small { display: block; } -.research-native-basis { font-size: var(--text-caption); } +.research-native-basis, .research-progress .research-native-basis p, .research-native-basis > summary { font-size: var(--text-body); } .research-usage-basis { display: grid; grid-template-columns: 140px minmax(0, 1fr); gap: 12px 24px; margin-block: 8px 20px; font-size: var(--text-body); line-height: var(--leading-prose); } .research-progress-basis dt, .research-usage-basis dt { color: var(--pw-muted); } .research-progress-basis dd, .research-usage-basis dd { margin: 0; } @@ -122,7 +131,8 @@ .research-progress select { max-width: 100%; } .research-effect-metrics { gap: 8px; } .research-assessment > summary { gap: 8px; } - .research-native-values { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .research-native-reading { align-items: flex-start; flex-direction: column; gap: 20px; } + .research-native-context { gap: 12px 24px; } .research-progress-values { grid-template-columns: 1fr; gap: 16px; } .research-request-values { grid-template-columns: 1fr; } .research-usage-basis { grid-template-columns: 1fr; gap: 4px; } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.tsx index 148c5affeb..d6cd03b4d0 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.tsx @@ -1,5 +1,5 @@ import {useId, useRef, useState} from "react"; -import {ArrowUpRight, ChevronDown, X} from "lucide-react"; +import {ArrowUpRight, ChevronDown, FileText, X} from "lucide-react"; import {TRAJECTORY_METRICS} from "../../../../../../loopx/control_plane/capabilities/trajectory_assessment_types.js"; import type {AssessmentCitation, AssessmentVerdict, TrajectoryAssessment, TrajectoryEpisode, TrajectoryInput, TrajectoryMetric, TrajectorySolverUsage} from "../../../../../../loopx/control_plane/capabilities/trajectory_assessment_types.js"; import type {TrajectoryNativeOutcomes} from "../../../../../../loopx/control_plane/capabilities/trajectory_native_outcomes.js"; @@ -121,27 +121,38 @@ function NativeResults({native, zh, onCitation}: {native: ResearchProgressPack[" const outcomes = native?.projection; if (!native || !outcomes) return

{zh ? "原生测量 · 未采集" : "Native measurements · not captured"}

; const historical = native.applicability === "historical"; - const points = [["initial", "首次提交", "Initial submission"], ["best_valid", "窗口内最佳有效值", "Best valid in capture"], ["retained", "原生保留结果", "Native retained result"], ["final_valid", "最后有效提交", "Final valid submission"]] as const; + const points = [["best_valid", "窗口内最佳有效值", "Best valid in capture"], ["initial", "首次提交", "Initial submission"], ["final_valid", "最后有效提交", "Final valid submission"], ["retained", "原生保留结果", "Native retained result"]] as const; + const period = new Intl.DateTimeFormat(zh ? "zh-CN" : "en", {timeZone: "UTC", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, + ...(new Date(outcomes.start).getUTCFullYear() !== new Date(outcomes.end).getUTCFullYear() ? {year: "numeric"} : {})}); + const value = ([key, cn, en]: typeof points[number]) => { + const point = outcomes[key]; + return
{zh ? cn : en}
{point ? + : {zh ? "未知" : "Unknown"}{key === "best_valid" ? · {outcomes.metric.unit} : null}}
; + }; return

{historical ? (zh ? "历史原生测量" : "Historical native measurements") : (zh ? "原生测量" : "Native measurements")}

{zh ? "最佳只限本次捕获窗口;原生保留与最后提交可能不同。未采集的值保留未知,不记零。" : "Best is limited to this capture; retained and final submissions can differ. Uncaptured values remain unknown, not zero."}

{zh ? "检验过收益、采用了证据及原目标达成各需各自证据,不能由这些数值推断。" : "Utility testing, evidence adoption and original-objective success each need their own evidence; these values cannot establish them."}

{historical ?

{zh ? "来源已变化或不可读;以下仅为历史捕获,当前结果未核验。" : "The source changed or is unavailable. These are historical captures; current results are unverified."}

: null} -

{outcomes.metric.name} · {outcomes.metric.unit} · {outcomes.metric.higher_is_better ? (zh ? "越高越好" : "higher is better") : (zh ? "越低越好" : "lower is better")} · {outcomes.valid_count} / {outcomes.sample_count} {zh ? "项有效结果" : "valid results"}

-

{outcomes.complete ? (zh ? "声明为完整前缀" : "Declared complete prefix") : (zh ? "部分窗口,起点未知" : "Partial window; initial unknown")} · —

-
{points.map(([key, cn, en]) => { - const point = outcomes[key]; - return
{zh ? cn : en}
{point ? <> - - {zh ? "提交" : "Submission"} {point.submission_index} - : {zh ? "— 未知" : "— Unknown"}}
; - })}
+
+
{value(points[0])}

{outcomes.metric.name} · {outcomes.metric.higher_is_better ? (zh ? "越高越好" : "higher is better") : (zh ? "越低越好" : "lower is better")}

+
{points.slice(1).map(value)}
+
+
{outcomes.valid_count} / {outcomes.sample_count} {zh ? "项有效结果" : "valid results"} + {period.formatRange(new Date(outcomes.start), new Date(outcomes.end))} UTC + {!outcomes.complete ? {zh ? "部分窗口 · 起点未知" : "Partial window · initial unknown"} : null} +
{zh ? "测量来源与限制" : "Measurement sources and limits"} -

{native.artifact}
{native.artifact_sha256}

+
{zh ? "捕获窗口" : "Capture window"}
—
{outcomes.complete ? (zh ? "声明为完整前缀" : "Declared complete prefix") : (zh ? "部分窗口,起点未知" : "Partial window; initial unknown")}
+
{zh ? "指标与单位" : "Metric and unit"}
{outcomes.metric.name} · {outcomes.metric.unit}
+
{zh ? "方法" : "Method"}
{outcomes.method}
+
{zh ? "来源" : "Source"}
{native.artifact}
{native.artifact_sha256}
{points.map(([key, cn, en]) => { const point = outcomes[key]; - return point ?
{zh ? cn : en}
{point.candidate_ref}
{zh ? "提交" : "Submitted"}: {point.submitted_at ?? (zh ? "未知" : "Unknown")}
{zh ? "收到结果" : "Result received"}: {point.observed_at}
: null; + return point ?
{zh ? cn : en}
{point.candidate_ref}
{zh ? "提交" : "Submission"} {point.submission_index} · {zh ? "提交时间" : "Submitted at"}: {point.submitted_at ?? (zh ? "未知" : "Unknown")}
{zh ? "收到结果" : "Result received"}: {point.observed_at}
: null; })}
diff --git a/docs/development/assets/research-native-results/README.md b/docs/development/assets/research-native-results/README.md index c52b0762f0..c553c58f65 100644 --- a/docs/development/assets/research-native-results/README.md +++ b/docs/development/assets/research-native-results/README.md @@ -32,3 +32,36 @@ return focus, unknown retention and invalid results are exercised by the same packaged scenario. Browsing starts no model or experiment and leaves the source unchanged. These captures qualify a proposed packaged surface, not installed adoption, user comprehension improvement or release readiness. + +## Native measurement hierarchy + +The UI source is `ee911667cbe664d7cbf8a914ee076d766a048c64`, based on main +`44050e953aa21f6deb932935fa158b5e415894c1`. The packaged capture run used +those source bytes; this documentation commit only records its visual evidence. + +The `hierarchy-desktop.png` and `hierarchy-mobile.png` captures use the same +native-only fixture as `after-desktop.png` and `after-mobile.png`. The capture's +best valid value is now primary; initial, last valid and actually retained +results are compact context. Unknown retention stays explicit. A document icon +opens original evidence without suggesting a trend. Raw unit, direction, +valid-result coverage and a concise UTC window remain visible. Full timestamps, +submission identities, method, source digest and limitations are available in +one disclosure with body-sized text. + +The `hierarchy-report-*.png` images show the same synthesis fixture as the +earlier ordinary report captures. The `hierarchy-sources-*.png` images use a +separate synthetic signed-value fixture: best, last and retained differ, so the +source view can be checked without treating best as adoption. These are whole +packaged workspace viewports, not live operating context or installed-App +readback. Desktop is 1280 × 900; narrow is 390 × 844. The narrow report view +uses the existing one-step report navigation; it is a selected reading position, +not the default top of the page. + +The existing research-history scenario covers ordinary/deep/history reading, +historical results and source switching. The focused `research-native-reading` +scenario crosses the same real registration, typed native projection, SQLite +store and HTTP reader for higher/lower directions, negative and zero values, +invalid first/all-invalid/empty samples, partial windows, distinct retained +results and long units. It also checks exact citation return, source disclosure, +Chinese/English help, narrow layout and 200% text. No model or experiment is +executed; original sources and mutation counts are checked separately. diff --git a/docs/development/assets/research-native-results/hierarchy-desktop.png b/docs/development/assets/research-native-results/hierarchy-desktop.png new file mode 100644 index 0000000000..b36efcaef0 Binary files /dev/null and b/docs/development/assets/research-native-results/hierarchy-desktop.png differ diff --git a/docs/development/assets/research-native-results/hierarchy-mobile.png b/docs/development/assets/research-native-results/hierarchy-mobile.png new file mode 100644 index 0000000000..f97616a8d6 Binary files /dev/null and b/docs/development/assets/research-native-results/hierarchy-mobile.png differ diff --git a/docs/development/assets/research-native-results/hierarchy-report-desktop.png b/docs/development/assets/research-native-results/hierarchy-report-desktop.png new file mode 100644 index 0000000000..ddccf9b70c Binary files /dev/null and b/docs/development/assets/research-native-results/hierarchy-report-desktop.png differ diff --git a/docs/development/assets/research-native-results/hierarchy-report-mobile.png b/docs/development/assets/research-native-results/hierarchy-report-mobile.png new file mode 100644 index 0000000000..72f5cbec80 Binary files /dev/null and b/docs/development/assets/research-native-results/hierarchy-report-mobile.png differ diff --git a/docs/development/assets/research-native-results/hierarchy-sources-desktop.png b/docs/development/assets/research-native-results/hierarchy-sources-desktop.png new file mode 100644 index 0000000000..7fcb87aff2 Binary files /dev/null and b/docs/development/assets/research-native-results/hierarchy-sources-desktop.png differ diff --git a/docs/development/assets/research-native-results/hierarchy-sources-mobile.png b/docs/development/assets/research-native-results/hierarchy-sources-mobile.png new file mode 100644 index 0000000000..3b09ab5c06 Binary files /dev/null and b/docs/development/assets/research-native-results/hierarchy-sources-mobile.png differ diff --git a/examples/personal-workspace-browser-smoke.mjs b/examples/personal-workspace-browser-smoke.mjs index 9bf71d6c7d..6d310b86fc 100644 --- a/examples/personal-workspace-browser-smoke.mjs +++ b/examples/personal-workspace-browser-smoke.mjs @@ -76,6 +76,7 @@ import { researchRecordSearchScenario } from "./personal-workspace-browser/resea import { researchRouteReadingScenario } from "./personal-workspace-browser/research-route-reading.mjs"; import { researchDisplayLanguageScenario } from "./personal-workspace-browser/research-display-language.mjs"; import { researchMeasurementsScenario } from "./personal-workspace-browser/research-measurements.mjs"; +import { researchNativeReadingScenario } from "./personal-workspace-browser/research-native-reading.mjs"; import { loopxModeScenario } from "./personal-workspace-browser/loopx-mode.mjs"; import { progressiveLoadingScenario } from "./personal-workspace-browser/progressive-loading.mjs"; import { stewardJourneyScenario } from "./personal-workspace-browser/steward-journey.mjs"; @@ -129,6 +130,7 @@ scenarioCatalog.push(attentionUserActionScenario); scenarioCatalog.push(researchResultsScenario); scenarioCatalog.push(researchHistoryScenario); scenarioCatalog.push(researchMeasurementsScenario); +scenarioCatalog.push(researchNativeReadingScenario); scenarioCatalog.push(researchFindingReaderScenario); scenarioCatalog.push(researchRecordSearchScenario); scenarioCatalog.push(researchRouteReadingScenario); diff --git a/examples/personal-workspace-browser/research-history.mjs b/examples/personal-workspace-browser/research-history.mjs index 881a7c2447..e04d0047be 100644 --- a/examples/personal-workspace-browser/research-history.mjs +++ b/examples/personal-workspace-browser/research-history.mjs @@ -137,7 +137,14 @@ export const researchHistoryScenario = { const native = nativePage.getByRole('region', {name: '原生测量', exact: true}); await native.waitFor(); assert.match(await native.innerText(), /2 \/ 3.*项有效结果/s); - assert.match(await native.innerText(), /首次提交.*7.*窗口内最佳有效值.*3.*原生保留结果.*未知/s); + assert.equal(await native.locator('[data-native-point="best_valid"] strong').textContent(), '3'); + assert.equal(await native.locator('[data-native-point="initial"] strong').textContent(), '7'); + assert.match(await native.locator('[data-native-point="retained"]').innerText(), /未知/); + assert(await native.locator('[data-native-point="best_valid"] strong').evaluate(e => parseFloat(getComputedStyle(e).fontSize) >= 32)); + assert(await native.locator('[data-native-point="initial"] strong').evaluate(e => parseFloat(getComputedStyle(e).fontSize) >= 16)); + assert.match(await native.locator('.research-native-scope').innerText(), /UTC/); + assert.doesNotMatch(await native.locator('.research-native-scope').innerText(), /T00:00:00Z/); + assert.equal(await native.locator('.research-native-basis').evaluate(e => e.open), false); assert.equal(await nativePage.locator('.research-progress-basis').evaluate(e => e.open), false); assert.equal(await nativePage.locator('.research-effect-empty').isVisible(), true, 'Measured progress remains readable before model analysis'); await nativePage.screenshot({path: resolve(outputDir, 'research-native-default-desktop.png'), animations: 'disabled'}); @@ -149,6 +156,7 @@ export const researchHistoryScenario = { await original.press('Escape'); assert.equal(await nativePage.locator('.research-analysis').count(), 0, 'Reading a native citation does not open or run model analysis'); await nativePage.setViewportSize({width: 390, height: 844}); + await native.getByRole('heading', {name: '原生测量', exact: true}).click(); await nativePage.evaluate(() => {for (const e of document.querySelectorAll('*')) if (e.scrollTop) e.scrollTop = 0;}); assert(await nativePage.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)); await nativePage.screenshot({path: resolve(outputDir, 'research-native-default-mobile.png'), animations: 'disabled'}); @@ -202,6 +210,8 @@ export const researchHistoryScenario = { await synthesisPage.evaluate(() => {for (const e of document.querySelectorAll('*')) if (e.scrollTop) e.scrollTop = 0;}); assert(await synthesisPage.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)); await synthesisPage.screenshot({path: resolve(outputDir, 'research-synthesis-ordinary-mobile.png'), animations: 'disabled'}); + await reading.getByRole('button', {name: '汇报', exact: true}).click(); + await synthesisPage.screenshot({path: resolve(outputDir, 'research-synthesis-report-mobile.png'), animations: 'disabled'}); await synthesisPage.getByRole('button', {name: '深入解读', exact: true}).click(); const analysis = synthesisPage.getByRole('region', {name: '轨迹深度分析', exact: true}); await analysis.waitFor(); @@ -889,8 +899,11 @@ Path(sys.argv[1]).write_text(json.dumps({'goal_id':goal,'display_name':'Syntheti await improvement.locator('summary').click(); await basis.locator(':scope > summary').click(); const nativeResults = progress.getByRole('region', {name: '原生测量', exact: true}); - assert.match(await nativeResults.innerText(), /首次提交.*7.*提交 1/s, 'Native measurements retain their source-bound endpoints'); - assert.match(await nativeResults.innerText(), /原生保留结果.*3.*提交 3/s); + assert.equal(await nativeResults.locator('[data-native-point="initial"] strong').textContent(), '7', 'Native measurements retain their source-bound endpoints'); + assert.equal(await nativeResults.locator('[data-native-point="retained"] strong').textContent(), '3'); + await nativeResults.locator('.research-native-basis > summary').click(); + assert.match(await nativeResults.locator('.research-native-basis').innerText(), /candidate-first.*提交 1/s, 'Exact submission identity is available on demand'); + await nativeResults.locator('.research-native-basis > summary').click(); assert.match(await nativeResults.innerText(), /越低越好/); await nativeResults.getByRole('button', {name: /首次提交.*查看原始证据/}).scrollIntoViewIfNeeded(); assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), 'Native details remain readable on a narrow screen'); @@ -980,8 +993,8 @@ Path(sys.argv[1]).write_text(json.dumps({'goal_id':goal,'display_name':'Syntheti assert.equal(await improvement.locator('[data-retained-improvement]').textContent(), '0', 'A rejected candidate does not create realized gain'); await basis.locator(':scope > summary').click(); assert.match(await nativeResults.innerText(), /历史原生测量.*当前结果未核验/s, 'Stale measurements never appear current'); - assert.match(await nativeResults.locator('[data-native-point="retained"]').innerText(), /3.*提交 3/s); - assert.match(await nativeResults.locator('[data-native-point="final_valid"]').innerText(), /5.*提交 4/s, 'The last submission is not silently presented as retained or best'); + assert.equal(await nativeResults.locator('[data-native-point="retained"] strong').textContent(), '3'); + assert.equal(await nativeResults.locator('[data-native-point="final_valid"] strong').textContent(), '5', 'The last submission is not silently presented as retained or best'); await page.reload({waitUntil: "networkidle"}); await analysis.locator("header p").first().filter({hasText: /^Synthetic analysis B$/}).waitFor(); await page.setViewportSize({width: 390, height: 844}); @@ -1017,6 +1030,7 @@ Path(sys.argv[1]).write_text(json.dumps({'goal_id':goal,'display_name':'Syntheti await analysis.locator("header p").first().filter({hasText: /^Synthetic analysis B$/}).waitFor(); await analysis.press("Escape"); await analysis.waitFor({state: "hidden"}); + await page.waitForFunction(() => document.activeElement?.textContent?.trim() === "查看分析"); assert.equal(await history.getByRole("button", {name: "查看分析", exact: true}).evaluate(button => button === document.activeElement), true); await progress.getByRole("combobox").focus(); await progress.getByRole("combobox").selectOption("c".repeat(64)); @@ -1054,9 +1068,9 @@ Path(sys.argv[1]).write_text(json.dumps({'goal_id':goal,'display_name':'Syntheti assert.equal(await analysis.count(), 0, 'Choosing an unanalysed source never requests analysis'); assert.equal(await improvement.locator('[data-retained-improvement]').textContent(), '—', 'Missing comparison evidence is unknown, never zero'); await basis.locator(':scope > summary').click(); - assert.match(await nativeResults.innerText(), /原生保留结果.*3.*提交 3/s, 'Native facts do not require an analyzer result'); - assert.match(await nativeResults.innerText(), /部分窗口,起点未知/); - assert.match(await nativeResults.locator('[data-native-point="initial"]').innerText(), /— 未知/); + assert.equal(await nativeResults.locator('[data-native-point="retained"] strong').textContent(), '3', 'Native facts do not require an analyzer result'); + assert.match(await nativeResults.innerText(), /部分窗口 · 起点未知/); + assert.match(await nativeResults.locator('[data-native-point="initial"]').innerText(), /未知/); const nativeCitation = nativeResults.getByRole('button', {name: /原生保留结果.*查看原始证据/}); await nativeCitation.focus(); await nativeCitation.press('Enter'); await citedOriginal.waitFor(); diff --git a/examples/personal-workspace-browser/research-native-reading.mjs b/examples/personal-workspace-browser/research-native-reading.mjs new file mode 100644 index 0000000000..9e9f3dec0d --- /dev/null +++ b/examples/personal-workspace-browser/research-native-reading.mjs @@ -0,0 +1,131 @@ +import assert from "node:assert/strict"; +import {createHash} from "node:crypto"; +import {mkdtemp, readFile, rm, writeFile} from "node:fs/promises"; +import {tmpdir} from "node:os"; +import {resolve} from "node:path"; +import {outputDir, repoRoot} from "./fixture.mjs"; +import {startHistoryOwner} from "./research-history.mjs"; + +// Real registration, native projection and HTTP readback. Independent expected +// values protect role identity: a highlighted best is never invented retention. +export const researchNativeReadingScenario = { + id: "research-native-reading", + async run({browser, collectCoverage}) { + const cases = [ + {id: "lower-negative", values: [-1, -3, -2], higher: false, retained: 0, expected: [-3, -1, -2, -1]}, + {id: "higher-zero", values: [-3, 0, -1], higher: true, retained: 0, expected: [0, -3, -1, -3]}, + {id: "invalid-first", values: [null, 0, -1], higher: true, retained: null, expected: [0, null, -1, null]}, + {id: "all-invalid", values: [null, null], higher: false, retained: null, expected: [null, null, null, null]}, + {id: "partial", values: [8, 7, 9], higher: true, retained: 1, partial: true, expected: [9, null, 9, 7]}, + {id: "empty", values: [], higher: true, retained: null, partial: true, expected: [null, null, null, null]}, + {id: "long-unit", values: [3, 1, 2], higher: false, retained: 0, unit: "synthetic-unit-".repeat(5), expected: [1, 3, 2, 3]}, + ]; + const root = await mkdtemp(resolve(tmpdir(), "loopx-native-reading-")); + let owner, page; + try { + const fixture = JSON.parse(await readFile(resolve(repoRoot, "apps/presentation/dashboard/src/data/fixtures/research-native-readback.json"), "utf8")); + const original = fixture.analyses[0]; + fixture.events = []; + fixture.analyses = cases.map(c => { + const a = structuredClone(original), native = JSON.parse(a.artifacts["native.json"]); + a.title = c.id; + a.input.scope.run_id = c.id; + native.scope = {...a.input.scope}; + native.complete = !c.partial; + native.metric.higher_is_better = c.higher; + if (c.unit) native.metric.unit = c.unit; + native.retained_result_id = c.retained === null ? null : `result-${c.retained}`; + native.samples = c.values.map((value, index) => ({ + submission_index: index + (c.partial ? 7 : 1), result_id: `result-${index}`, candidate_ref: `candidate-${index}`, + submitted_at: null, observed_at: `2026-01-01T00:0${index + 1}:00Z`, valid: value !== null, value, + citation: {source_id: "facts", first_line: index + 1, last_line: index + 1}, + })); + const text = c.values.map((v, i) => `Synthetic observation ${i}: value ${v ?? "unknown"}; valid ${v !== null}.`).join("\n") || "No measurements captured."; + a.artifacts["facts.txt"] = text; + a.artifacts["native.json"] = JSON.stringify(native); + Object.assign(a.input.sources[0], {text, artifact_sha256: createHash("sha256").update(text).digest("hex")}); + return a; + }); + const path = resolve(root, "cases.json"); + await writeFile(path, JSON.stringify(fixture)); + owner = await startHistoryOwner(path); + page = await browser.newPage({viewport: {width: 1280, height: 900}, hasTouch: true}); + const errors = []; + page.on("pageerror", error => errors.push(error.message)); + if (collectCoverage) await page.coverage.startJSCoverage({resetOnNavigation: false}); + await page.addInitScript(() => {if (!localStorage.getItem("loopx-pw-locale")) localStorage.setItem("loopx-pw-locale", "zh-CN");}); + await page.goto(`${owner.base}/chat/?goalId=${fixture.goal_id}&view=research&statusUrl=%2Fstatus.json`, {waitUntil: "domcontentloaded"}); + const native = page.getByRole("region", {name: "原生测量", exact: true}); + const selector = page.locator(".research-progress select"); + for (const c of cases) { + await selector.selectOption({label: c.id}); + const selected = await selector.inputValue(); + await page.locator(`.research-progress-scope[data-pack-id="${selected}"]`).waitFor(); + await native.waitFor(); + for (const [index, key] of ["best_valid", "initial", "final_valid", "retained"].entries()) { + const reading = native.locator(`[data-native-point="${key}"]`); + if (c.expected[index] === null) { + assert.equal(await reading.locator("button").count(), 0); + assert.match(await reading.innerText(), /未知/); + } else assert.equal(await reading.locator("strong").textContent(), String(c.expected[index])); + } + assert.match(await native.innerText(), c.higher ? /越高越好/ : /越低越好/); + assert.match(await native.locator(".research-native-best .research-native-unit").textContent(), new RegExp(c.unit ?? "points"), "The raw unit remains visible even when no valid measurement exists"); + assert.match(await native.locator(".research-native-scope").innerText(), new RegExp(`${c.values.filter(v => v !== null).length} / ${c.values.length}.*有效结果`)); + if (c.partial) assert.match(await native.innerText(), /部分窗口 · 起点未知/); + await page.setViewportSize({width: 390, height: 844}); + const details = native.locator(".research-native-basis"); + if (await details.evaluate(e => e.open)) await details.locator(":scope > summary").click(); + await details.locator(":scope > summary").focus(); + await page.keyboard.press("Enter"); + assert.match(await details.innerText(), /2026-01-01T00:00:00Z/); + assert(await details.evaluate(e => parseFloat(getComputedStyle(e).fontSize) >= 14)); + assert(await native.evaluate(e => e.scrollWidth <= e.clientWidth + 1)); + if (c.id === "lower-negative") { + await native.evaluate(e => e.scrollIntoView({block: "start"})); + await page.screenshot({path: resolve(outputDir, "research-native-sources-phone.png"), animations: "disabled"}); + await page.setViewportSize({width: 1280, height: 900}); + await page.screenshot({path: resolve(outputDir, "research-native-sources-desktop.png"), animations: "disabled"}); + await page.setViewportSize({width: 390, height: 844}); + await details.locator("li").last().scrollIntoViewIfNeeded(); + await page.screenshot({path: resolve(outputDir, "research-native-source-tail-phone.png"), animations: "disabled"}); + } + await details.locator(":scope > summary").tap(); + if (c.id === "lower-negative") { + const retained = native.getByRole("button", {name: /原生保留结果.*查看原始证据/}); + await retained.focus(); await retained.press("Enter"); + const citation = page.getByRole("region", {name: "引用原文", exact: true}); + await citation.waitFor(); + await citation.getByText(/Synthetic observation 0: value -1/).waitFor(); + assert.match(await citation.innerText(), /Synthetic observation 0: value -1/); + await citation.press("Escape"); + await page.waitForFunction(() => document.activeElement?.getAttribute("aria-label")?.startsWith("原生保留结果:")); + await page.screenshot({path: resolve(outputDir, "research-native-negative-phone.png"), animations: "disabled"}); + } + if (c.unit) { + await page.evaluate(() => document.documentElement.style.fontSize = "200%"); + assert(await native.evaluate(e => e.scrollWidth <= e.clientWidth + 1), "Long unit remains readable at 200% text"); + await page.screenshot({path: resolve(outputDir, "research-native-large-text-phone.png"), animations: "disabled"}); + await page.evaluate(() => document.documentElement.style.removeProperty("font-size")); + } + await page.setViewportSize({width: 1280, height: 900}); + } + await page.evaluate(() => localStorage.setItem("loopx-pw-locale", "en")); + await page.reload({waitUntil: "domcontentloaded"}); + await page.getByRole("region", {name: "Native measurements", exact: true}).waitFor(); + await page.getByRole("button", {name: "About native measurements", exact: true}).focus(); + await page.getByRole("tooltip").waitFor({state: "visible"}); + await page.keyboard.press("Escape"); + const readback = await (await page.request.get(`${owner.base}/fixture/readback`)).json(); + assert.equal(readback.source_unchanged, true); + assert.equal(readback.mutation_attempts, 0); + assert.deepEqual(errors, []); + return {coverageEntries: collectCoverage ? await page.coverage.stopJSCoverage() : [], + note: "real-owner native result roles: signed/zero/invalid/missing/partial, exact citations, readable desktop/390/200%, Chinese and English; no mutation"}; + } finally { + await page?.close(); + await owner?.close(); + await rm(root, {recursive: true, force: true}); + } + }, +};