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 ?
+ onCitation(point.citation, event.currentTarget)} aria-label={`${zh ? cn : en}: ${String(point.value)} ${outcomes.metric.unit} · ${zh ? "查看原始证据" : "Read original evidence"}`}>
+ {String(point.value)} {key === "best_valid" ? {outcomes.metric.unit} : null}
+ : {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")} · {outcomes.start} — {outcomes.end}
- {points.map(([key, cn, en]) => {
- const point = outcomes[key];
- return {zh ? cn : en} {point ? <>
- onCitation(point.citation, event.currentTarget)} aria-label={`${zh ? cn : en}: ${String(point.value)} · ${zh ? "查看原始证据" : "Read original evidence"}`}>{String(point.value)}
- {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.start} — {outcomes.end} {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;
})}
{outcomes.limitations.map((line, i) => {line} )}
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});
+ }
+ },
+};