diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.tsx
index 425cfde938..ecf4f70d5a 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.tsx
@@ -219,11 +219,12 @@ export function ResearchAnalysisPanel({analysis: a}: {analysis: Controller}) {
const label = metricCopy[row.metric];
return {label?.[zh ? 0 : 1] ?? row.metric} {verdictLabel(row.metric, row.verdict, zh)} {label?.[zh ? 2 : 3]}
{row.reason}
{citationButtons(row.citations)} ;
})})}
- {a.citationBusy ?
{zh ? "正在读取引用原文…" : "Reading cited source…"}
: null}
- {a.citationError ? {a.citationError}
: null}
- {a.citation ? {if (event.key === "Escape") {event.stopPropagation(); a.closeCitation();}}}>{zh ? "引用原文" : "Cited original"} {a.citation.artifact} · {zh ? "行" : "lines"} {a.citation.first_line}–{a.citation.last_line}
{a.citation.text} : null}
{zh ? "来源、方法、覆盖与分析成本" : "Sources, method, coverage and analysis cost"} {p.objective}
{zh ? "来源范围" : "Source scope"} {p.scope.case_id} · {p.scope.run_id} {p.scope.window} {zh ? "方法" : "Method"} {result.method_version} · {zh ? "模型判断未校准" : "Uncalibrated model judgment"} {zh ? "模型设置" : "Requested model"} {p.config.model} / {p.config.effort} / {p.config.service_tier} · {zh ? "实际服务档位" : "Served tier"}: {p.job?.served_service_tier ?? (zh ? "未知" : "unknown")} {zh ? "分析用量" : "Analysis usage"} {p.job?.analysis_wall_seconds == null ? "—" : `${p.job.analysis_wall_seconds.toFixed(1)}s`} · {zh ? "输入" : "input"} {p.job?.usage?.input_tokens?.toLocaleString() ?? "—"} / {zh ? "输出" : "output"} {p.job?.usage?.output_tokens?.toLocaleString() ?? "—"} tokens · {zh ? "金额未采集,推理 token 已包含于输出" : "Dollar cost unmeasured; reasoning tokens are included in output"} {zh ? "覆盖" : "Coverage"} {p.episode_count} {zh ? "个固定片段" : "fixed episodes"} · {p.source_count} {zh ? "份来源" : "sources"} · {zh ? "总体片段数" : "Population"}: {p.selection.population_episodes ?? (zh ? "未知" : "unknown")} {zh ? "取样规则" : "Selection"} {p.selection.rule} {zh ? "版本" : "Revision"} {p.scope.revision} {result.limitations.map((text, i) => {text} )} {zh ? "引用行存在不等于解释正确;本页不提供独立科学校准结论。" : "A valid citation range does not establish entailment or independent scientific calibration."}
> : null}
+ {/* Bound native evidence is readable before any model assessment exists. */}
+ {a.citationBusy ? {zh ? "正在读取引用原文…" : "Reading cited source…"}
: null}
+ {a.citationError ? {a.citationError}
: null}
+ {a.citation ? {if (event.key === "Escape") {event.stopPropagation(); a.closeCitation();}}}>{zh ? "引用原文" : "Cited original"} {a.citation.artifact} · {zh ? "行" : "lines"} {a.citation.first_line}–{a.citation.last_line}
{a.citation.text} : null}
> : null}
;
}
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 69d6f38306..d4179411a3 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,13 +1,17 @@
import {ArrowUpRight, ChevronDown} 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";
import "./goal-research-progress.css";
// Read models from benchmark-toolkit. This view formats existing counts and
// assessments; it never scores, reclassifies an episode or combines runs.
export type ResearchProgressPack = {pack_id: string; title: string; scope: TrajectoryInput["scope"]; selection: TrajectoryInput["selection"];
episode_count: number; episodes?: TrajectoryEpisode[]; result?: TrajectoryAssessment | null; applicability?: "current" | "historical";
- solver_usage?: {artifact: string; artifact_sha256: string; projection: TrajectorySolverUsage; applicability: "current" | "historical"; reason_code: string | null}};
+ job?: {status: string; attempt?: number; analysis_wall_seconds?: number | null;
+ usage?: {input_tokens?: number; output_tokens?: number} | null; config?: {model: string; effort: string; service_tier: string}} | null;
+ solver_usage?: {artifact: string; artifact_sha256: string; projection: TrajectorySolverUsage; applicability: "current" | "historical"; reason_code: string | null};
+ native_outcomes?: {artifact: string; artifact_sha256: string; projection: TrajectoryNativeOutcomes; applicability: "current" | "historical"; reason_code: string | null}};
export const metricCopy: Record = {
goal_alignment: ["目标关联", "Goal alignment", "干预是否关联原目标的真实瓶颈", "Does the intervention address a bottleneck of the original objective?"],
benefit_test: ["收益检验", "Benefit test", "是否真正检验原目标收益,而非只完成前置诊断", "Was original-objective utility tested, beyond diagnostic prerequisites?"],
@@ -25,6 +29,7 @@ export function verdictLabel(metric: TrajectoryMetric, verdict: AssessmentVerdic
export function ResearchProgress({pack: p, packs, selectedId, error, zh, onSelect, onCitation}: {pack: ResearchProgressPack | null; packs: Pick[]; selectedId: string; error: string; zh: boolean; onSelect: (id: string) => void; onCitation: (citation: AssessmentCitation, origin?: HTMLButtonElement) => void}) {
const measured = p?.solver_usage, usage = measured?.projection, result = p?.result;
+ const native = p?.native_outcomes, outcomes = native?.projection;
const number = (n: number) => new Intl.NumberFormat(zh ? "zh-CN" : "en", {maximumFractionDigits: 1}).format(n);
const percent = (n: number) => new Intl.NumberFormat(zh ? "zh-CN" : "en", {style: "percent", maximumFractionDigits: 0}).format(n);
const duration = (s: number) => `${number(s / 60)} ${zh ? "分钟" : "min"}`;
@@ -67,10 +72,28 @@ export function ResearchProgress({pack: p, packs, selectedId, error, zh, onSelec
{zh ? "题目 / 运行" : "Case / run"} {p.scope.case_id} {p.scope.run_id} {zh ? "证据窗口" : "Evidence window"} {p.scope.window} {zh ? "取样规则" : "Selection rule"} {p.selection.rule}
{zh ? "总体研究片段数" : "Population of episodes"} {p.selection.population_episodes == null ? (zh ? "未知" : "Unknown") : number(p.selection.population_episodes)}
{result ? <>{zh ? "分析方法" : "Analysis method"} {result.method_version} · {zh ? "Agent 判读未校准" : "Uncalibrated Agent judgment"} {zh ? "结果标识" : "Result key"} {result.analysis_key}{zh ? "分析限制" : "Limitations"} {result.limitations.map((line, i) => {line}
)} > : null}
- {zh ? "来源版本" : "Source revision"} {p.scope.revision}{zh ? "原生收益" : "Native utility"} {zh ? "此包尚未接入原生起点、保留最佳、最后有效值、达标时间/成本或增益面积投影。不能从模型判读或成本补算。" : "This pack has no native incumbent, retained-best, final-valid, threshold time/cost or gain-area projection. Model judgments and usage cannot supply them."}
+ {zh ? "来源版本" : "Source revision"} {p.scope.revision}
+ {zh ? "原生结果" : "Native outcomes"} {outcomes ? <>
+ {outcomes.metric.name} · {outcomes.metric.unit} · {outcomes.metric.higher_is_better ? (zh ? "越高越好" : "higher is better") : (zh ? "越低越好" : "lower is better")}
+ {outcomes.start} — {outcomes.end} · {outcomes.complete ? (zh ? "声明为完整前缀" : "Declared complete prefix") : (zh ? "部分窗口,起点未知" : "Partial window; initial unknown")} · {outcomes.valid_count} / {outcomes.sample_count} {zh ? "项有效结果" : "valid results"}
+ {([ ["initial", "首次提交", "Initial submission"], ["best_valid", "窗口内最佳有效值", "Best valid in capture"], ["retained", "原生保留结果", "Native retained result"], ["final_valid", "最后有效提交", "Final valid submission"] ] as const).map(([key, cn, en]) => {
+ const point = outcomes[key];
+ return
{zh ? cn : en} {point ? <>{String(point.value)} · {point.candidate_ref} onCitation(point.citation, event.currentTarget)}>{zh ? "查看原始证据" : "Read original evidence"} > : (zh ? "— 未知,不记零" : "— Unknown, not zero")} ;
+ })}
+ {native?.applicability === "historical" ? {zh ? "原生来源已变化或不可读;仅保留历史捕获,当前结果未核验。" : "Native source changed or is unavailable; retained historical capture, current results unverified."}
: null}
+ {zh ? "原生测量与分析器判断分开。有效性和保留选择来自提供方,不认证研究因果收益;用量窗口独立,不补算达标成本或全程账单。" : "Native measurements are separate from analyzer judgments. Validity and retention are provider-recorded, not causal benefit. Usage has its own window; threshold and total-study costs are not inferred."}
+ > : (zh ? "— 尚无同范围的原生结果捕获;不能从分析器判断或用量补算。" : "— No native capture for this scope; analyzer judgments and usage cannot supply it.")}
{zh ? "用量与成本" : "Usage and cost"}{usage ? ` · ${zh ? "捕获跨度" : "Captured elapsed"} ${duration(usage.elapsed_seconds)}` : ` · ${zh ? "尚无完整用量捕获" : "No complete usage capture"}`}{measured?.applicability === "historical" ? (zh ? " · 历史捕获" : " · Historical capture") : ""}
{zh ? "只读捕获,窗口可能与以上判读片段及下方 Explore 历史不同。" : "Read-only capture; its window can differ from the judgments above and Explore history below."}
+
+ {zh ? "分析尝试开销" : "Analysis attempt cost"} {p.job?.usage ? <>
+ {zh ? "第" : "Attempt"} {p.job.attempt ?? "—"} · {zh ? "输入" : "input"} {p.job.usage.input_tokens == null ? "—" : number(p.job.usage.input_tokens)} · {zh ? "输出" : "output"} {p.job.usage.output_tokens == null ? "—" : number(p.job.usage.output_tokens)} tokens
+ {p.job.config ? ` · ${p.job.config.model}` : ""}
+ {p.job.analysis_wall_seconds != null ? ` · ${duration(p.job.analysis_wall_seconds)}` : ""}
+ > : (zh ? "— 本次分析尝试未测得,不记零" : "— No measurement for this analysis attempt; not zero")}
+ {zh ? "合计边界" : "Aggregation boundary"} {zh ? "分析仅显示当前回执的本次尝试;不含更早失败、恢复、评测或操作者开销。不与求解捕获盲目相加,金额及完整研究成本仍未知。" : "Analysis is this attempt's receipt only; earlier failures, recovery, evaluation and operator work are excluded. It is not blindly added to solver capture. Monetary and complete study costs remain unknown."}
+
{zh ? "捕获窗口跨度" : "Captured elapsed time"} {usage ? duration(usage.elapsed_seconds) : "—"} {usage ? `${number(usage.completed_turns)} ${zh ? "轮完整记录,不是全程总成本" : "complete Turns, not total study cost"}` : (zh ? "尚无完整用量捕获" : "No complete usage capture")}
{zh ? "未缓存输入" : "Uncached input"} {usage ? number(usage.uncached_input_tokens) : "—"} {usage ? "tokens" : (zh ? "未测,不记零" : "Unmeasured, not zero")}
diff --git a/docs/architecture/rfcs/goal-workspace-surface-v0.md b/docs/architecture/rfcs/goal-workspace-surface-v0.md
index c389c34b4e..9290498c5d 100644
--- a/docs/architecture/rfcs/goal-workspace-surface-v0.md
+++ b/docs/architecture/rfcs/goal-workspace-surface-v0.md
@@ -732,6 +732,16 @@ independent scientific calibration or completion of V12. Host registration
remains preparation outside the ordinary reading journey. CLI preparation uses
the producer's existing interface; no new interactive Lark path is qualified.
+The follow-up native-readback proposal extends that same trusted registration
+with an optional provider outcome capture, scoped to the exact case/run/revision.
+The existing source disclosure separates first submission, best valid in capture,
+native-retained and final valid values, with original citations and historical/
+partial/unknown states. Submission order, not feedback arrival, identifies the
+candidate. This is not another scorer or automatic solver feedback. Captured
+solver usage and the current analysis attempt's receipt stay separate; full-study
+cost and causal benefit remain unknown. No first-viewport/hero/CTA change is
+introduced; six uncalibrated process judgments retain their existing role.
+
## 12. Open decisions
| # | Decision | Owner | Options | Recommendation | Needed before |
diff --git a/docs/architecture/rfcs/goal-workspace-surface-v0.zh-CN.md b/docs/architecture/rfcs/goal-workspace-surface-v0.zh-CN.md
index fd7c9204ce..210fd5cf90 100644
--- a/docs/architecture/rfcs/goal-workspace-surface-v0.zh-CN.md
+++ b/docs/architecture/rfcs/goal-workspace-surface-v0.zh-CN.md
@@ -520,6 +520,12 @@ producer 接入一次显式分析、取消、重试和有引用的结果阅读
任意 run 自动准备、独立科学校准或 V12 完成。host 注册仍是普通阅读旅程之外的准备,
CLI 准备复用 producer 既有接口;本切片未验收新增飞书交互入口。
+后续原生读回提案在同一可信注册入口增加可选 provider 结果捕获,固定题目、运行和
+来源版本。既有来源展开分别展示首次提交、捕获内最佳有效值、原生保留和最后有效提交,
+含原文引用及历史/部分/未知状态;候选按提交顺序而非反馈到达排序。不新增评分器或
+自动 solver 回灌。求解捕获与本次分析尝试的回执开销分别保留,完整研究成本及因果收益
+仍未知;首屏、hero、CTA 不变,六项未校准的过程判断维持原有角色。
+
## 12. 未决事项
| # | 事项 | Owner | 选项 | 建议 | 需在何时前决定 |
diff --git a/docs/development/assets/research-analysis/native-outcomes-desktop.png b/docs/development/assets/research-analysis/native-outcomes-desktop.png
new file mode 100644
index 0000000000..c30c1bb2d0
Binary files /dev/null and b/docs/development/assets/research-analysis/native-outcomes-desktop.png differ
diff --git a/docs/development/assets/research-analysis/native-outcomes-mobile.png b/docs/development/assets/research-analysis/native-outcomes-mobile.png
new file mode 100644
index 0000000000..bd5ca958fb
Binary files /dev/null and b/docs/development/assets/research-analysis/native-outcomes-mobile.png differ
diff --git a/docs/reference/trajectory-assessment.md b/docs/reference/trajectory-assessment.md
index 2c26a91eb3..d145976f80 100644
--- a/docs/reference/trajectory-assessment.md
+++ b/docs/reference/trajectory-assessment.md
@@ -192,6 +192,77 @@ journey or qualify the model assessor.
## Measured solver session cost (optional)
+### Native outcome readback (optional, separate from analyzer judgments)
+
+The trusted provider/Agent may add `--native-outcomes-json native-outcomes.json`
+to the existing `benchmark trajectory-pack-register` operation. The file is
+relative to its already authorized `--source-root`; the renderer cannot register
+paths or invent measurements. Reading it does not execute analysis or change a
+solver, evaluator, retained archive or study board. Omitting it preserves the
+previous pack identity and response shape.
+
+The provider-exported JSON has this local diagnostic contract:
+
+```json
+{
+ "schema_version": "trajectory_native_outcomes_v1",
+ "scope": {"goal_id":"solver", "case_id":"case", "run_id":"run", "revision":"fixed", "window":"declared capture"},
+ "start": "2026-01-01T00:00:00Z",
+ "end": "2026-01-01T00:10:00Z",
+ "complete": true,
+ "metric": {"name":"error", "unit":"points", "higher_is_better":false},
+ "retained_result_id": "result-1",
+ "samples": [{
+ "submission_index":1, "result_id":"result-1", "candidate_ref":"candidate-a",
+ "submitted_at":"2026-01-01T00:01:00Z", "observed_at":"2026-01-01T00:02:00Z",
+ "valid":true, "value":2,
+ "citation":{"source_id":"native-source", "first_line":1, "last_line":1}
+ }]
+}
+```
+
+Every scope field must equal the registered assessment input. Source IDs and
+line ranges must belong to that input; measurement/candidate IDs are opaque.
+UTC timestamps must name real calendar instants (including leap-day validity),
+not dates that a JavaScript parser silently normalizes into another window.
+The source must contain provider-recorded released outcomes, not assessor
+ratings or hidden evaluator material. The input curator/provider remains
+responsible for the claim's truth and validity. `complete` declares prefix
+coverage, not official integrity or score countability. A complete prefix has
+contiguous submission indices starting at one. An invalid record uses
+`valid:false,value:null`; zero is a valid measured value, never a missing value.
+`retained_result_id:null` means retention is unknown, not that the mathematically
+best value was adopted.
+
+The existing typed benchmark-toolkit prepare owner returns a separate bounded
+`native_outcomes` projection: first valid submission-one value when the prefix
+is complete, best valid measurement within the capture under the explicit native
+metric direction, provider-selected retained result, and final valid submission.
+These are distinct: delayed feedback does not promote an older submission to
+the final candidate, and a better measured score does not override retention or
+guardrails. No generic score, monetary estimate or causal benefit is derived.
+The original analysis key/model input is unchanged; native capture provenance
+instead versions the pack identity. Changed/unavailable files leave historical
+captured values visible with their warning, rather than borrowing another pack.
+
+The existing Goal research source disclosure renders those facts and opens only
+their bound original citations, even before model analysis exists. Solver
+capture, native outcome and analyzed-episode windows remain separate. The cost
+disclosure also shows the current analysis attempt's observed receipt usage;
+earlier failures, native evaluation, recovery, operator costs and unmeasured
+windows are not silently combined into a full-study bill. This does not complete
+threshold-cost, gain-area, matched-study or scientific qualification.
+
+Synthetic packaged native readback (not benchmark findings):
+
+
+
+[Narrow-screen source disclosure](../development/assets/research-analysis/native-outcomes-mobile.png).
+The existing six-judgment first viewport is unchanged; these facts live in its
+existing source disclosure, with costs in their separate disclosure.
+
+### Solver capture registration
+
A trusted pack curator can bind a **complete captured Codex session** separately
from the model evidence. The existing register command accepts:
diff --git a/examples/personal-workspace-browser/research-history.mjs b/examples/personal-workspace-browser/research-history.mjs
index c566ae1cdf..adbde8cb8c 100644
--- a/examples/personal-workspace-browser/research-history.mjs
+++ b/examples/personal-workspace-browser/research-history.mjs
@@ -24,7 +24,15 @@ function analysisReadback(selected = "a") {
turns: [{turn_id: "synthetic-turn", start: "2026-01-01T00:00:00Z", end: "2026-01-01T00:01:01Z", first_line: 2, last_line: 3, elapsed_seconds: 61, usage: {input_tokens: 1100, cached_input_tokens: 1000, uncached_input_tokens: 100, output_tokens: 20}}], limitations: []}}}),
episodes: selected === "c" ? [{id: "episode", question: "Is this experiment discriminative?", source_ids: ["source"], eligible_metrics: ["discrimination"]}]
: [{id: "episode", question: "Does a useful diagnosis prove original-objective utility?", source_ids: ["source"], eligible_metrics: ["goal_alignment", "benefit_test", "discrimination", "information_gain", "evidence_adoption", "redundancy"]}, {id: "uncertain", question: "Was the later decision informed by this evidence?", source_ids: ["source"], eligible_metrics: ["evidence_adoption"]}],
- job: {status: "succeeded", analysis_wall_seconds: null, usage: null},
+ ...(["a", "b", "d"].includes(selected) ? {native_outcomes: {artifact: "native-outcomes.json", artifact_sha256: "9".repeat(64),
+ applicability: selected === "b" ? "historical" : "current", reason_code: selected === "b" ? "source_changed" : null,
+ projection: {method: "source_bound_native_outcomes_v1", scope, start: "2026-01-01T00:00:00Z", end: "2026-01-01T00:10:00Z",
+ complete: true, metric: {name: "error", unit: "points", higher_is_better: false}, sample_count: 3, valid_count: 2,
+ ...Object.fromEntries(["initial", "best_valid", "retained", "final_valid"].map(key => [key, {submission_index: key === "initial" ? 1 : 3,
+ result_id: key === "initial" ? "native-initial" : "native-final", candidate_ref: key === "initial" ? "candidate-first" : "candidate-retained",
+ submitted_at: "2026-01-01T00:01:00Z", observed_at: "2026-01-01T00:02:00Z", value: key === "initial" ? 7 : 3,
+ citation: {source_id: "source", first_line: 1, last_line: 2}}])), limitations: []}}} : {}),
+ job: {status: "succeeded", attempt: 2, analysis_wall_seconds: 12, usage: {input_tokens: 30, output_tokens: 4}, config},
result: {schema_version: "trajectory_assessment_v1", method_version: "research-episodes-v2", analysis_key: (selected === "c" ? "e" : "c").repeat(64), scope, selection,
calibration: "uncalibrated_model", evidence_manifest: [{id: "source", artifact: "facts.txt", artifact_sha256: "f".repeat(64), first_line: 1}], limitations: ["Synthetic presentation fixture; not a scientific result"],
metrics: [...(selected === "c" ? [{metric: "benefit_test", yes: 0, no: 0, unknown: 0, eligible: 0, assessed_fraction: null, missing_data_bounds: null}, {metric: "discrimination", yes: 0, no: 0, unknown: 1, eligible: 1, assessed_fraction: 0, missing_data_bounds: [0, 1]}]
@@ -318,7 +326,24 @@ Path(sys.argv[1]).write_text(json.dumps({'goal_id':goal,'display_name':'Syntheti
await progress.locator('.research-usage > summary').click();
assert.equal(await progress.getByText("100", {exact: true}).count(), 1, "Uncached input is shown without charging cache twice");
assert.match(await progress.innerText(), /1,?100.*1,?000/s);
+ assert.match(await progress.innerText(), /30.*4.*tokens/s, "Analysis attempt cost is distinct from solver usage");
+ assert.match(await progress.innerText(), /更早失败.*恢复.*评测/s);
await progress.locator('.research-usage > summary').click();
+ const basis = progress.locator('.research-progress-basis');
+ await basis.locator('summary').click();
+ assert.match(await basis.innerText(), /首次提交.*7.*candidate-first/s);
+ assert.match(await basis.innerText(), /原生保留结果.*3.*candidate-retained/s);
+ await basis.getByRole('button', {name: '查看原始证据', exact: true}).first().scrollIntoViewIfNeeded();
+ assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), 'Native details remain readable on a narrow screen');
+ await page.screenshot({path: resolve(outputDir, "research-native-outcomes-mobile.png"), animations: "disabled"});
+ await page.setViewportSize({width:1280,height:900});
+ await page.screenshot({path: resolve(outputDir, "research-native-outcomes-desktop.png"), animations: "disabled"});
+ await basis.getByRole('button', {name: '查看原始证据', exact: true}).first().click();
+ await citedOriginal.waitFor();
+ assert.match(await citedOriginal.innerText(), /Synthetic original evidence/);
+ await citedOriginal.press('Escape');
+ await history.getByRole('button', {name: '关闭分析', exact: true}).click();
+ await basis.locator('summary').click();
await benefitMetric.locator('summary').click();
assert.match(await progress.innerText(), /0 \/ 1/);
assert.match(await progress.innerText(), /1 \/ 2/);
@@ -367,6 +392,14 @@ Path(sys.argv[1]).write_text(json.dumps({'goal_id':goal,'display_name':'Syntheti
assert.match(await progress.innerText(), /此范围尚无分析器判读/);
assert.equal(await progress.locator('.research-effect-metrics').count(), 0, 'An unanalysed cost pack never borrows another pack’s judgments');
assert.equal(await analysis.count(), 0, 'Choosing an unanalysed source never requests analysis');
+ await basis.locator('summary').click();
+ assert.match(await basis.innerText(), /原生保留结果.*candidate-retained/s, 'Native facts do not require an analyzer result');
+ const nativeCitation = basis.getByRole('button', {name: '查看原始证据', exact: true}).first();
+ await nativeCitation.focus(); await nativeCitation.press('Enter');
+ await citedOriginal.waitFor();
+ await citedOriginal.press('Escape');
+ await history.getByRole('button', {name: '关闭分析', exact: true}).click();
+ await basis.locator('summary').click();
// Analysis sources do not require an Explore log. The real read-only
// backend must keep their measurements discoverable with zero events.
const emptyFixture = resolve(temporary, "empty.json");
diff --git a/loopx/capabilities/benchmark_toolkit/trajectory_assessment_store.py b/loopx/capabilities/benchmark_toolkit/trajectory_assessment_store.py
index 87161963c2..461c588997 100644
--- a/loopx/capabilities/benchmark_toolkit/trajectory_assessment_store.py
+++ b/loopx/capabilities/benchmark_toolkit/trajectory_assessment_store.py
@@ -52,6 +52,8 @@ def _pack_id(owner_goal_id: str, record: dict) -> str:
}
if "solver_usage" in record:
identity["solver_usage"] = record["solver_usage"]
+ if "native_outcomes" in record:
+ identity["native_outcomes"] = record["native_outcomes"]
return _digest(identity)
@@ -146,8 +148,8 @@ def _usage_capture(source_root: Path, artifact: str, session_id: str) -> dict:
}
-def _usage_view(record: dict) -> dict | None:
- captured = record.get("solver_usage")
+def _captured_view(record: dict, field: str) -> dict | None:
+ captured = record.get(field)
if captured is None:
return None
reason = None
@@ -223,7 +225,7 @@ def _summary(record: dict, reason: str | None) -> dict:
"config": record["config"],
"availability": "unavailable" if reason else "available",
"reason_code": reason,
- **({"solver_usage": _usage_view(record)} if "solver_usage" in record else {}),
+ **({field: _captured_view(record, field) for field in ("solver_usage", "native_outcomes") if field in record}),
}
@@ -237,6 +239,7 @@ def register_pack(
config: dict | None = None,
usage_jsonl: str | None = None,
usage_session_id: str | None = None,
+ native_outcomes_json: str | None = None,
) -> dict:
"""Trusted operator only; no HTTP/renderer path-registration endpoint."""
if not title.strip():
@@ -266,6 +269,17 @@ def register_pack(
record["solver_usage"] = _usage_capture(
source_root, usage_jsonl, usage_session_id
)
+ if native_outcomes_json is not None:
+ data = _source_path(source_root, native_outcomes_json).read_bytes()
+ measured = effect_runtime_result("benchmark.trajectory.prepare", {
+ "input": record["input"], "config": record["config"],
+ "native_outcomes": json.loads(data),
+ })["native_outcomes"]
+ record["native_outcomes"] = {
+ "artifact": native_outcomes_json,
+ "artifact_sha256": hashlib.sha256(data).hexdigest(),
+ "projection": measured,
+ }
owner = _owner(runtime_root, owner_goal_id)
(owner / "packs").mkdir(parents=True, exist_ok=True, mode=0o700)
pack_id = _pack_id(owner_goal_id, record)
@@ -445,12 +459,15 @@ def read_citation(
runtime_root=runtime_root, owner_goal_id=owner_goal_id, pack_id=pack_id
)
result = view["result"]
- if result is None or not any(
+ citations = [c for a in (result or {}).get("assessments", []) for c in a["citations"]]
+ native = (view.get("native_outcomes") or {}).get("projection") or {}
+ native_citations = [native[key]["citation"] for key in ("initial", "best_valid", "retained", "final_valid") if native.get(key)]
+ requested = {"source_id": source_id, "first_line": first_line, "last_line": last_line}
+ if not any(
c["source_id"] == source_id
and c["first_line"] == first_line
and c["last_line"] == last_line
- for a in result["assessments"]
- for c in a["citations"]
+ for c in [*citations, *native_citations]
):
raise PackUnavailable("citation_not_in_result")
source = next(s for s in record["input"]["sources"] if s["id"] == source_id)
@@ -465,5 +482,6 @@ def read_citation(
first_line - source["first_line"] : last_line - source["first_line"] + 1
]
),
- "applicability": view["applicability"],
+ "applicability": "historical" if requested in native_citations
+ and view["native_outcomes"]["applicability"] == "historical" else view["applicability"],
}
diff --git a/loopx/cli_commands/benchmark_trajectory.py b/loopx/cli_commands/benchmark_trajectory.py
index e6d91ea7ff..d1a333964f 100644
--- a/loopx/cli_commands/benchmark_trajectory.py
+++ b/loopx/cli_commands/benchmark_trajectory.py
@@ -41,6 +41,10 @@ def register_benchmark_trajectory_commands(subparsers, add_format) -> None:
"--usage-session-id",
help="Expected captured solver session identity; required with --usage-jsonl.",
)
+ register.add_argument(
+ "--native-outcomes-json",
+ help="Optional provider-recorded native outcomes, relative to source-root; same pack scope, never analyzer scores.",
+ )
add_format(register)
listing = subparsers.add_parser(
"trajectory-packs",
@@ -87,6 +91,7 @@ def _handle_benchmark_trajectory_command(
title=args.title,
usage_jsonl=args.usage_jsonl,
usage_session_id=args.usage_session_id,
+ native_outcomes_json=args.native_outcomes_json,
)
if args.benchmark_command == "trajectory-pack-register"
else discover(**scope)
diff --git a/loopx/control_plane/capabilities/trajectory_assessment.ts b/loopx/control_plane/capabilities/trajectory_assessment.ts
index 49a5abae5f..3ae78e633d 100644
--- a/loopx/control_plane/capabilities/trajectory_assessment.ts
+++ b/loopx/control_plane/capabilities/trajectory_assessment.ts
@@ -1,5 +1,6 @@
/** Benchmark-toolkit's observer sidecar; never a solver or native-score authority. */
import {createHash} from 'node:crypto';
+import {projectTrajectoryNativeOutcomes} from './trajectory_native_outcomes.ts';
import {BARE_SHA256_PATTERN} from '../content_digest.ts';
import {EffectRuntimeRequestError} from '../effect_runtime_errors.ts';
@@ -94,6 +95,7 @@ export function prepareTrajectoryAssessment(params: Record) {
const analysis_key = createHash('sha256').update(JSON.stringify({method: TRAJECTORY_METHOD, rubric: RUBRIC, input, config: configIdentity})).digest('hex');
const numberedSources = input.sources.map(s => ({...s, text: s.text.split('\n').map((line, n) => `${s.first_line+n}: ${line}`).join('\n')}));
return {analysis_key, method_version: TRAJECTORY_METHOD, config: configIdentity, input,
+ ...(params.native_outcomes === undefined ? {} : {native_outcomes: projectTrajectoryNativeOutcomes(params.native_outcomes, input)}),
prompt: `${RUBRIC}\n\nEvidence pack:\n${JSON.stringify({...input, sources: numberedSources})}`, output_schema: TRAJECTORY_OUTPUT_SCHEMA};
}
export function qualifyTrajectoryAssessment(params: Record): TrajectoryAssessment {
diff --git a/loopx/control_plane/capabilities/trajectory_native_outcomes.ts b/loopx/control_plane/capabilities/trajectory_native_outcomes.ts
new file mode 100644
index 0000000000..cac3e837d2
--- /dev/null
+++ b/loopx/control_plane/capabilities/trajectory_native_outcomes.ts
@@ -0,0 +1,99 @@
+/** Source-bound native measurements, not analyzer judgments or score authority. */
+import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
+import type {AssessmentCitation, TrajectoryInput} from "./trajectory_assessment_types.ts";
+
+export interface NativeOutcomePoint {
+ submission_index: number; result_id: string; candidate_ref: string;
+ submitted_at: string; observed_at: string; value: number; citation: AssessmentCitation;
+}
+export interface TrajectoryNativeOutcomes {
+ method: "source_bound_native_outcomes_v1";
+ scope: TrajectoryInput["scope"]; start: string; end: string; complete: boolean;
+ metric: {name: string; unit: string; higher_is_better: boolean};
+ sample_count: number; valid_count: number;
+ initial: NativeOutcomePoint | null; best_valid: NativeOutcomePoint | null;
+ retained: NativeOutcomePoint | null; final_valid: NativeOutcomePoint | null;
+ limitations: string[];
+}
+
+function fail(message: string): never { throw new EffectRuntimeRequestError(message); }
+function object(value: unknown, label: string): Record {
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail(`${label} must be an object`);
+ return value as Record;
+}
+function identifier(value: unknown, label: string): string {
+ if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/.test(value)) {
+ fail(`${label} must be an opaque public-safe identifier`);
+ }
+ return value;
+}
+function metricLabel(value: unknown, label: string): string {
+ if (typeof value !== "string" || !value.trim() || value.length > 80 || /[\u0000-\u001f]/.test(value)) {
+ fail(`${label} must be a bounded display label`);
+ }
+ return value;
+}
+function timestamp(value: unknown, label: string): string {
+ if (typeof value !== "string" || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,3})?Z$/.test(value)
+ || !Number.isFinite(Date.parse(value))) fail(`${label} must be a UTC timestamp`);
+ // Date.parse normalizes impossible calendar dates and 24:00 into another
+ // instant. Compare canonical milliseconds, accepting the declared precision
+ // without silently moving the provider's measurement window.
+ const canonical = value.slice(0, 19) + "." + (value.slice(19, -1).slice(1) || "").padEnd(3, "0") + "Z";
+ if (new Date(value).toISOString() !== canonical) fail(`${label} must be a UTC timestamp`);
+ return value;
+}
+
+/** The provider declares validity/retention. This owner checks binding and ranks
+ * valid measurements within that declared capture, never certifying a study. */
+export function projectTrajectoryNativeOutcomes(value: unknown, input: TrajectoryInput): TrajectoryNativeOutcomes {
+ const raw = object(value, "native outcomes"), scope = object(raw.scope, "native outcome scope");
+ if (raw.schema_version !== "trajectory_native_outcomes_v1") fail("unsupported native outcomes schema");
+ if (Object.keys(input.scope).some(key => scope[key] !== input.scope[key as keyof typeof input.scope])
+ || Object.keys(scope).length !== Object.keys(input.scope).length) fail("native outcome scope differs from the registered pack");
+ const start = timestamp(raw.start, "native start"), end = timestamp(raw.end, "native end");
+ if (Date.parse(start) > Date.parse(end) || typeof raw.complete !== "boolean") fail("native capture requires an ordered window and explicit coverage");
+ const metric = object(raw.metric, "native metric");
+ const name = metricLabel(metric.name, "native metric name"), unit = metricLabel(metric.unit, "native metric unit");
+ if (typeof metric.higher_is_better !== "boolean") fail("native metric direction must be explicit");
+ if (!Array.isArray(raw.samples) || raw.samples.length > 10000) fail("native samples must be an array of at most 10000 records");
+ const valid: NativeOutcomePoint[] = [], seen = new Set();
+ let previous = 0;
+ for (const value of raw.samples) {
+ const sample = object(value, "native sample"), index = sample.submission_index;
+ if (!Number.isSafeInteger(index) || Number(index) <= previous
+ || raw.complete && index !== previous + 1) fail("native submission indices must be distinct, ordered and complete when declared complete");
+ previous = Number(index);
+ const result = identifier(sample.result_id, "native result"), candidate = identifier(sample.candidate_ref, "native candidate");
+ if (seen.has(result)) fail("native result identity is duplicated");
+ seen.add(result);
+ const submitted = timestamp(sample.submitted_at, "native submitted_at"), observed = timestamp(sample.observed_at, "native observed_at");
+ if (Date.parse(submitted) < Date.parse(start) || Date.parse(observed) > Date.parse(end)
+ || Date.parse(submitted) > Date.parse(observed)) fail("native result lies outside its capture or precedes submission");
+ if (typeof sample.valid !== "boolean" || sample.valid && (typeof sample.value !== "number" || !Number.isFinite(sample.value))
+ || !sample.valid && sample.value !== null) fail("native validity and measured value must agree; missing/invalid is not zero");
+ const citation = object(sample.citation, "native citation"), source = input.sources.find(s => s.id === citation.source_id);
+ const first = citation.first_line, last = citation.last_line;
+ if (!source || !Number.isSafeInteger(first) || !Number.isSafeInteger(last)
+ || Number(first) < source.first_line || Number(last) < Number(first)
+ || Number(last) >= source.first_line + source.text.split("\n").length) fail("native citation is outside registered evidence");
+ if (sample.valid) valid.push({submission_index: Number(index), result_id: result, candidate_ref: candidate,
+ submitted_at: submitted, observed_at: observed, value: sample.value as number,
+ citation: {source_id: source.id, first_line: Number(first), last_line: Number(last)}});
+ }
+ let best: NativeOutcomePoint | null = null;
+ for (const point of valid) if (!best || (metric.higher_is_better ? point.value > best.value : point.value < best.value)) best = point;
+ const retainedId = raw.retained_result_id;
+ if (retainedId !== null) identifier(retainedId, "retained native result");
+ const retained = valid.find(point => point.result_id === retainedId) ?? null;
+ if (retainedId !== null && !retained) fail("retained native result must be a valid result in this capture");
+ return {method: "source_bound_native_outcomes_v1", scope: input.scope, start, end, complete: raw.complete,
+ metric: {name, unit, higher_is_better: metric.higher_is_better}, sample_count: raw.samples.length,
+ valid_count: valid.length, initial: raw.complete ? valid.find(point => point.submission_index === 1) ?? null : null,
+ best_valid: best, retained, final_valid: valid.at(-1) ?? null,
+ limitations: ["Provider-recorded measurements in the declared capture; not study countability or a causal effect.",
+ "Validity and retained selection belong to the native provider, not model judgments or Todo status.",
+ "Initial requires a complete prefix and a valid first submission; invalid or missing results are not zero.",
+ "Submission order determines initial/final; delayed feedback does not make an older candidate current.",
+ "Costs retain their own measured windows; no monetary, whole-study or per-result cost is inferred."]};
+}
diff --git a/tests/capabilities/test_trajectory_assessment_store.py b/tests/capabilities/test_trajectory_assessment_store.py
index 21e96f8164..6479f0b99b 100644
--- a/tests/capabilities/test_trajectory_assessment_store.py
+++ b/tests/capabilities/test_trajectory_assessment_store.py
@@ -9,6 +9,7 @@
from datetime import datetime, timedelta, timezone
import pytest
+from loopx.control_plane.effect_runtime import EffectRuntimeRejected
from loopx.capabilities.benchmark_toolkit.trajectory_assessment_store import (
PackUnavailable,
@@ -134,6 +135,62 @@ def test_scope_discovery_execute_read_citation_and_changed_source_history(pack):
)
+def test_native_results_register_through_real_cli_and_read_without_analysis(pack, tmp_path):
+ scope, _, original, input_path, sources, _ = pack
+ native = {
+ "schema_version": "trajectory_native_outcomes_v1",
+ "scope": json.loads(input_path.read_text())["scope"],
+ "start": "2026-01-01T00:00:00Z", "end": "2026-01-01T00:10:00Z",
+ "complete": True, "metric": {"name": "error", "unit": "points", "higher_is_better": False},
+ "retained_result_id": "native-1",
+ "samples": [{"submission_index": 1, "result_id": "native-1", "candidate_ref": "candidate-a",
+ "submitted_at": "2026-01-01T00:01:00Z", "observed_at": "2026-01-01T00:02:00Z",
+ "valid": True, "value": 2, "citation": {"source_id": "source", "first_line": 2, "last_line": 2}}],
+ }
+ native_path = sources / "native-outcomes.json"
+ native_path.write_text(json.dumps(native))
+ registry = tmp_path / "native-registry.json"
+ registry.write_text(json.dumps({"goals": []}))
+ result = subprocess.run([sys.executable, "-m", "loopx.cli", "--registry", str(registry),
+ "--runtime-root", str(scope["runtime_root"]), "--format", "json", "benchmark", "trajectory-pack-register",
+ "--goal-id", scope["owner_goal_id"], "--input-json", str(input_path), "--source-root", str(sources),
+ "--title", "Native outcome window", "--native-outcomes-json", native_path.name],
+ capture_output=True, text=True, check=True)
+ registered = json.loads(result.stdout)
+ assert registered["analysis_key"] == original["analysis_key"]
+ assert "native_outcomes" not in original
+ assert registered["native_outcomes"]["projection"]["initial"]["value"] == 2
+ view = inspect_pack(**scope, pack_id=registered["pack_id"])
+ assert view["job"] is None and view["result"] is None
+ assert view["native_outcomes"]["projection"]["retained"]["candidate_ref"] == "candidate-a"
+ citation = read_citation(**scope, pack_id=registered["pack_id"], analysis_key=registered["analysis_key"],
+ source_id="source", first_line=2, last_line=2)
+ assert citation["text"] == "The native measurement did not improve utility."
+ with pytest.raises(PackUnavailable, match="citation_not_in_result"):
+ read_citation(**scope, pack_id=registered["pack_id"], analysis_key=registered["analysis_key"],
+ source_id="source", first_line=1, last_line=2)
+ native["samples"][0]["value"] = 3
+ native_path.write_text(json.dumps(native))
+ old = inspect_pack(**scope, pack_id=registered["pack_id"])
+ assert old["native_outcomes"]["applicability"] == "historical"
+ assert old["native_outcomes"]["projection"]["initial"]["value"] == 2
+ assert read_citation(**scope, pack_id=registered["pack_id"], analysis_key=registered["analysis_key"],
+ source_id="source", first_line=2, last_line=2)["applicability"] == "historical"
+ assert not list((scope["runtime_root"] / "benchmark" / "trajectory-assessments").glob("*/jobs/*"))
+
+
+def test_native_capture_cannot_borrow_another_run_or_escape_source_root(pack):
+ scope, _, _, input_path, sources, _ = pack
+ path = sources / "wrong-native.json"
+ raw = {"schema_version": "trajectory_native_outcomes_v1", "scope": {
+ **json.loads(input_path.read_text())["scope"], "run_id": "foreign"}}
+ path.write_text(json.dumps(raw))
+ with pytest.raises(EffectRuntimeRejected, match="scope differs"):
+ register_pack(**scope, input_path=input_path, source_root=sources, title="wrong", native_outcomes_json=path.name)
+ with pytest.raises(PackUnavailable, match="invalid_source_path"):
+ register_pack(**scope, input_path=input_path, source_root=sources, title="wrong", native_outcomes_json="../input.json")
+
+
def test_owner_isolation_unknown_ids_and_keys_are_rejected(pack):
scope, operation, _, _, _, _ = pack
other = {**scope, "owner_goal_id": "other-owner"}
diff --git a/tests/control_plane_ts/trajectory_native_outcomes.test.ts b/tests/control_plane_ts/trajectory_native_outcomes.test.ts
new file mode 100644
index 0000000000..df176b9763
--- /dev/null
+++ b/tests/control_plane_ts/trajectory_native_outcomes.test.ts
@@ -0,0 +1,73 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import {prepareTrajectoryAssessment, type TrajectoryInput} from "../../loopx/control_plane/capabilities/trajectory_assessment.ts";
+
+const input: TrajectoryInput = {schema_version:"trajectory_assessment_input_v1",
+ scope:{goal_id:"g",case_id:"c",run_id:"r",revision:"rev",window:"complete"}, objective:"Minimize native error",
+ selection:{rule:"all",population_episodes:1,exhaustive:true},
+ sources:[{id:"s",artifact:"native.json",artifact_sha256:"a".repeat(64),first_line:1,text:"first\nold delayed result\nnew retained result"}],
+ episodes:[{id:"e",question:"Does the alternative help?",source_ids:["s"],eligible_metrics:["benefit_test"]}]};
+const config = {model:"fixture",effort:"max",service_tier:"fast"};
+function capture() {return {schema_version:"trajectory_native_outcomes_v1",scope:{...input.scope},
+ start:"2026-01-01T00:00:00Z",end:"2026-01-01T00:10:00Z",complete:true,
+ metric:{name:"error",unit:"points",higher_is_better:false},retained_result_id:"result-3",
+ samples:[2,5,1].map((value,i)=>({submission_index:i+1,result_id:`result-${i+1}`,candidate_ref:`candidate-${i+1}`,
+ submitted_at:`2026-01-01T00:0${i}:00Z`,observed_at:`2026-01-01T00:0${i===1?5:3}:00Z`,valid:true,value,
+ citation:{source_id:"s",first_line:i+1,last_line:i+1}}))};}
+function project(value: unknown) {return prepareTrajectoryAssessment({input,config,native_outcomes:value}).native_outcomes!;}
+
+test("native facts preserve first quality, actual retention and submission order independently of delayed feedback",()=>{
+ const native=project(capture());
+ assert.equal(native.initial?.value,2); assert.equal(native.best_valid?.value,1);
+ assert.equal(native.retained?.result_id,"result-3"); assert.equal(native.final_valid?.result_id,"result-3");
+ assert.equal(native.sample_count,3); assert.equal(native.valid_count,3);
+ assert.equal(prepareTrajectoryAssessment({input,config,native_outcomes:capture()}).analysis_key,
+ prepareTrajectoryAssessment({input,config}).analysis_key,"native readback does not rerun unchanged analysis");
+ assert.equal(Object.hasOwn(prepareTrajectoryAssessment({input,config}),"native_outcomes"),false);
+});
+test("higher-is-better is explicit; best measurement does not invent retained selection",()=>{
+ const raw=capture();raw.metric.higher_is_better=true;raw.retained_result_id="result-1";
+ raw.metric.name="Native throughput (%)"; raw.metric.unit="ops/s";
+ const native=project(raw);assert.equal(native.best_valid?.value,5);assert.equal(native.retained?.value,2);
+ assert.equal(native.metric.unit,"ops/s");assert.equal(native.metric.name,"Native throughput (%)");
+});
+test("invalid first measurements and incomplete prefixes remain unknown, never zero",()=>{
+ const raw=capture() as unknown as Record;
+ raw.samples[0].valid=false;raw.samples[0].value=null;
+ assert.equal(project(raw).initial,null);assert.equal(project(raw).valid_count,2);
+ raw.complete=false;raw.samples=raw.samples.slice(1);
+ assert.equal(project(raw).initial,null);assert.equal(project(raw).final_valid?.value,1);
+ raw.samples[0].valid=false;raw.samples[0].value=0;assert.throws(()=>project(raw),/validity/);
+});
+test("wrong source, duplicate results, old scope and invented retention cannot borrow a pack",()=>{
+ for(const mutate of [
+ (x:any)=>x.scope.run_id="foreign", (x:any)=>x.scope.revision="old",
+ (x:any)=>x.samples[1].result_id=x.samples[0].result_id,
+ (x:any)=>x.samples[1].submission_index=1,
+ (x:any)=>x.samples[0].citation.last_line=4,
+ (x:any)=>x.samples[0].citation.source_id="foreign",
+ (x:any)=>x.samples[0].observed_at="2026-01-01T00:11:00Z",
+ (x:any)=>x.retained_result_id="invented",
+ (x:any)=>x.samples[0].value=Infinity,
+ ]) {const x=capture();mutate(x);assert.throws(()=>project(x));}
+});
+test("UTC dates must name real calendar instants, preserving legal fractional seconds",()=>{
+ for (const date of ["2026-02-30T00:00:00Z", "2025-02-29T00:00:00Z",
+ "2026-04-31T00:00:00Z", "2026-01-01T24:00:00Z"]) {
+ for (const field of ["start", "end", "submitted_at", "observed_at"]) {
+ const raw = capture() as any;
+ if (field === "start" || field === "end") raw[field] = date;
+ else raw.samples[0][field] = date;
+ assert.throws(()=>project(raw), /UTC timestamp/, `${field}: ${date}`);
+ }
+ }
+ for (const fraction of ["", ".1", ".12", ".123"]) {
+ const raw = capture();
+ raw.start = `2024-02-29T00:00:00${fraction}Z`;
+ raw.end = "2024-03-01T00:00:00Z";
+ for (const sample of raw.samples) {
+ sample.submitted_at = sample.observed_at = raw.start;
+ }
+ assert.equal(project(raw).initial?.submitted_at, raw.start);
+ }
+});
diff --git a/tests/presentation/test_research_analysis_api.py b/tests/presentation/test_research_analysis_api.py
index c76d3a86e4..5e83c008c4 100644
--- a/tests/presentation/test_research_analysis_api.py
+++ b/tests/presentation/test_research_analysis_api.py
@@ -149,3 +149,27 @@ def test_http_rejects_other_owner_paths_free_configuration_and_wrong_origin(work
with pytest.raises(HTTPError):
read()
assert not w["count"].exists()
+
+
+def test_native_outcomes_and_bound_citations_read_over_http_without_analysis(workspace):
+ w = workspace
+ data = json.loads((w["tmp_path"] / "input.json").read_text())
+ native = {"schema_version": "trajectory_native_outcomes_v1", "scope": data["scope"],
+ "start": "2026-01-01T00:00:00Z", "end": "2026-01-01T00:03:00Z", "complete": True,
+ "metric": {"name": "error", "unit": "points", "higher_is_better": False},
+ "retained_result_id": "result-one", "samples": [{"submission_index": 1,
+ "result_id": "result-one", "candidate_ref": "candidate-one", "valid": True, "value": 0.000001,
+ "submitted_at": "2026-01-01T00:01:00Z", "observed_at": "2026-01-01T00:02:00Z",
+ "citation": {"source_id": "facts", "first_line": 2, "last_line": 2}}]}
+ (w["sources"] / "native.json").write_text(json.dumps(native))
+ registered = register_pack(runtime_root=w["tmp_path"] / "runtime", owner_goal_id="research-owner",
+ input_path=w["tmp_path"] / "input.json", source_root=w["sources"], title="Native capture", native_outcomes_json="native.json")
+ snapshot = w["request"](pack_id=registered["pack_id"])
+ assert snapshot["pack"]["native_outcomes"]["projection"]["retained"]["value"] == 0.000001
+ assert snapshot["pack"]["job"] is None
+ citation = w["request"](suffix="/citation", pack_id=registered["pack_id"], analysis_key=registered["analysis_key"],
+ source_id="facts", first_line=2, last_line=2)
+ assert citation["text"] == data["sources"][0]["text"]
+ assert not w["count"].exists()
+ with pytest.raises(HTTPError):
+ w["request"](goal="other-owner", pack_id=registered["pack_id"])