From c0a4f824551b52b199bceacc302a39cf877d4cf6 Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Mon, 12 Oct 2026 03:57:56 +0800
Subject: [PATCH] fix(app): retain published research source and review
reference
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
.../decision-research-surface-coverage.tsx | 63 ++++++++++++-
.../dashboard/src/data/decision-research.ts | 15 ++++
.../goal-research-results.tsx | 9 +-
.../src/views/decision-research-surface.tsx | 89 ++++++++++++-------
4 files changed, 139 insertions(+), 37 deletions(-)
diff --git a/apps/presentation/dashboard/smoke/decision-research-surface-coverage.tsx b/apps/presentation/dashboard/smoke/decision-research-surface-coverage.tsx
index 7ea240333d..64b56077b6 100644
--- a/apps/presentation/dashboard/smoke/decision-research-surface-coverage.tsx
+++ b/apps/presentation/dashboard/smoke/decision-research-surface-coverage.tsx
@@ -1,10 +1,13 @@
import assert from "node:assert/strict";
import { renderToStaticMarkup } from "react-dom/server";
-import type { DecisionResearchView } from "../src/data/decision-research";
+import { fetchPresentationProjection, parsePresentationProjection, publishedResearchLineage, type DecisionResearchView } from "../src/data/decision-research";
+import projectionFixture from "../src/data/fixtures/presentation-projection.example.json";
+import { presentationSurfaceSchema } from "../src/data/status";
import {
SourcePeriodMetricsCard,
SpotMarketIdentityCard,
+ DecisionResearchSurface,
} from "../src/views/decision-research-surface";
type SourcePeriodMetric = DecisionResearchView["source_period_metrics"][number];
@@ -133,4 +136,60 @@ assert.match(spotMarkup, /SYN \/ USDC/);
assert.match(spotMarkup, /4.25/);
assert.match(spotMarkup, /missing \(not zero\)/);
-console.log("Decision research source-period rendering coverage passed");
+const projection = parsePresentationProjection(projectionFixture);
+const surface = presentationSurfaceSchema.parse({
+ extension_id: projection.extension_id, extension_revision: projection.extension_revision,
+ surface_id: projection.surface_id, surface_kind: projection.surface_kind,
+ title: "Synthetic research", view_schema: projection.view_schema, visibility: "public-safe",
+ state: "review_due", goal_id: projection.goal_id, generated_at: projection.generated_at,
+ review_due_at: projection.review_due_at, diagnostic: null,
+ empty_state_title: "No research", empty_state_detail: "No projection",
+ detail_ref: {extension_id: projection.extension_id, surface_id: projection.surface_id,
+ extension_revision: projection.extension_revision, payload_sha256: projection.payload_sha256},
+});
+const markup = renderToStaticMarkup();
+for (const value of [projection.goal_id, projection.generated_at, projection.review_due_at!,
+ "synthetic-research-2026-01-15", "Source version", "synthetic-previous-source",
+ projection.extension_revision, projection.payload_sha256, "does not extend the review deadline"]) {
+ assert(markup.includes(value), `Published reference must retain ${value}`);
+}
+assert.match(markup, /]*data-testid="research-reference-details"/);
+assert.deepEqual(publishedResearchLineage(), {sourceId: null, version: null, supersedes: null});
+assert.deepEqual(publishedResearchLineage({...projection, lineage: {source_id: false, version: "1", supersedes: false}}),
+ {sourceId: null, version: null, supersedes: null});
+const legacy = {...projection, review_due_at: null, lineage: undefined};
+const legacyMarkup = renderToStaticMarkup();
+assert.match(legacyMarkup, /原复核期限/);
+assert.match(legacyMarkup, /未知/);
+assert(!legacyMarkup.includes("synthetic-research-2026-01-15"), "Absent lineage must not be guessed from the view");
+
+const originalFetch = globalThis.fetch;
+let reply = projection;
+globalThis.fetch = async () => new Response(JSON.stringify({ok: true, projection: reply}));
+try {
+ assert.deepEqual(await fetchPresentationProjection("http://localhost/extension-projection", surface), projection);
+ for (const changes of [
+ {payload_sha256: "0".repeat(64)}, {extension_revision: "old-revision"},
+ {goal_id: "other-goal"}, {generated_at: "2026-01-16T12:00:00Z"},
+ {review_due_at: "2026-03-15T12:00:00Z"},
+ ]) {
+ reply = {...projection, ...changes};
+ await assert.rejects(fetchPresentationProjection("http://localhost/extension-projection", surface),
+ /does not match the requested detail_ref/);
+ }
+ reply = projection;
+ assert.deepEqual(await fetchPresentationProjection("http://localhost/extension-projection", surface), projection);
+ await assert.rejects(fetchPresentationProjection("http://localhost/extension-projection",
+ {...surface, visibility: "owner-only"}), /public-safe/);
+ await assert.rejects(fetchPresentationProjection("http://localhost/extension-projection",
+ {extension_id: surface.extension_id, extension_revision: surface.extension_revision,
+ surface_id: surface.surface_id, surface_kind: surface.surface_kind, title: surface.title,
+ view_schema: surface.view_schema, visibility: "public-safe", goal_id: null, generated_at: null,
+ review_due_at: null, diagnostic: null, empty_state_title: "Empty", empty_state_detail: "Disabled", state: "empty"}),
+ /no published projection/);
+} finally {globalThis.fetch = originalFetch;}
+
+console.log("Decision research source-period and published-reference coverage passed");
diff --git a/apps/presentation/dashboard/src/data/decision-research.ts b/apps/presentation/dashboard/src/data/decision-research.ts
index a11780a5ea..25076b4fa3 100644
--- a/apps/presentation/dashboard/src/data/decision-research.ts
+++ b/apps/presentation/dashboard/src/data/decision-research.ts
@@ -386,6 +386,19 @@ export type PresentationProjectionEnvelope = z.infer<
typeof presentationProjectionEnvelopeSchema
>;
+/** Lineage comes from the resolved envelope, never the status summary or view. */
+export function publishedResearchLineage(projection?: PresentationProjectionEnvelope | null) {
+ const lineage = projection?.lineage;
+ const source = z.string().min(1).refine(value => value.trim().length > 0).safeParse(lineage?.source_id);
+ const version = z.number().int().positive().safeParse(lineage?.version);
+ const supersedes = z.array(z.string().min(1)).max(20).safeParse(lineage?.supersedes);
+ return {
+ sourceId: source.success ? source.data : null,
+ version: version.success ? version.data : null,
+ supersedes: supersedes.success ? supersedes.data : null,
+ };
+}
+
export function parsePresentationProjection(
payload: unknown,
): PresentationProjectionEnvelope {
@@ -434,6 +447,8 @@ export async function fetchPresentationProjection(
|| projection.view_schema !== surface.view_schema
|| projection.surface_kind !== surface.surface_kind
|| projection.goal_id !== surface.goal_id
+ || projection.generated_at !== surface.generated_at
+ || projection.review_due_at !== surface.review_due_at
) {
throw new Error("projection response does not match the requested detail_ref");
}
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-results.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-results.tsx
index 69c29a765f..580aef9029 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-results.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-results.tsx
@@ -1,11 +1,11 @@
import {useEffect, useState} from "react";
import {RefreshCw} from "lucide-react";
-import {fetchPresentationProjection, type DecisionResearchView} from "../../data/decision-research";
+import {fetchPresentationProjection, type PresentationProjectionEnvelope} from "../../data/decision-research";
import {parsePresentationSurfaceCollectionResponse, type PresentationSurface, type RunGoal} from "../../data/status";
import {DecisionResearchSurface} from "../../views/decision-research-surface";
export type GoalResearchApi = {indexUrl: string; detailUrl: string; goals: RunGoal[]};
-type Result = {surface: PresentationSurface; view?: DecisionResearchView; error?: string};
+type Result = {surface: PresentationSurface; projection?: PresentationProjectionEnvelope; error?: string};
/** The active Goal's published research; the extension index owns availability. */
export function GoalResearchResults({goalId, api, zh}: {goalId: string; api: GoalResearchApi; zh: boolean}) {
@@ -29,7 +29,7 @@ export function GoalResearchResults({goalId, api, zh}: {goalId: string; api: Goa
if (surface.state !== "ready" && surface.state !== "review_due") return {surface};
try {
const projection = await fetchPresentationProjection(api.detailUrl, surface);
- return {surface, view: projection.view};
+ return {surface, projection};
} catch (failure) {return {surface, error: String(failure)};}
}));
if (current) setResults(next);
@@ -50,6 +50,7 @@ export function GoalResearchResults({goalId, api, zh}: {goalId: string; api: Goa
{error ? {zh ? "无法核验研究结果;旧内容已清除。" : "Cannot verify research results; previous content was cleared."} {error}
: null}
{!busy && !error && !results.length ? {zh ? "本 Goal 暂无可读取的公开研究成果。" : "No readable public research results for this Goal yet."}
: null}
{results.map(result => ) }
+ goals={api.goals} surface={result.surface} view={result.projection?.view}
+ projection={result.projection} viewError={result.error} zh={zh}/>) }
;
}
diff --git a/apps/presentation/dashboard/src/views/decision-research-surface.tsx b/apps/presentation/dashboard/src/views/decision-research-surface.tsx
index 9cbccd0591..14c72fd66b 100644
--- a/apps/presentation/dashboard/src/views/decision-research-surface.tsx
+++ b/apps/presentation/dashboard/src/views/decision-research-surface.tsx
@@ -1,7 +1,11 @@
import { CircleAlert, Clock3, FileSearch, ShieldCheck } from "lucide-react";
import type { PresentationSurface, RunGoal } from "../data/status";
-import type { DecisionResearchView } from "../data/decision-research";
+import {
+ publishedResearchLineage,
+ type DecisionResearchView,
+ type PresentationProjectionEnvelope,
+} from "../data/decision-research";
import { Badge } from "../components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
@@ -63,10 +67,26 @@ function TextList({ empty, items }: { empty: string; items: string[] }) {
);
}
-function ProvenanceCard({ surface }: { surface: PresentationSurface }) {
+function ProvenanceCard({ surface, projection, zh }: {
+ surface: PresentationSurface;
+ projection?: PresentationProjectionEnvelope | null;
+ zh: boolean;
+}) {
if (surface.state !== "ready" && surface.state !== "review_due") {
return null;
}
+ const lineage = publishedResearchLineage(projection);
+ const unknown = zh ? "未知" : "unknown";
+ const references = [
+ [zh ? "原 Goal" : "Original Goal", surface.goal_id],
+ [zh ? "来源编号" : "Source ID", lineage.sourceId],
+ [zh ? "来源版本" : "Source version", lineage.version],
+ [zh ? "替代来源" : "Supersedes", lineage.supersedes === null ? null
+ : lineage.supersedes.length ? lineage.supersedes.join(", ") : zh ? "无" : "none"],
+ [zh ? "扩展 / 展示面" : "Extension / surface", `${surface.extension_id} / ${surface.surface_id}`],
+ [zh ? "展示 schema" : "View schema", surface.view_schema],
+ [zh ? "扩展 revision" : "Extension revision", surface.detail_ref.extension_revision],
+ ];
return (
@@ -77,8 +97,8 @@ function ProvenanceCard({ surface }: { surface: PresentationSurface }) {
{surface.visibility}
- Rendered from the published projection resolved by content-addressed
- reference. Core status remains a compact provider-neutral index.
+ {zh ? "已发布研究的原始时间与来源引用;刷新页面不延长复核期限。"
+ : "Original publication times and source reference. Refreshing does not extend the review deadline."}
@@ -86,41 +106,44 @@ function ProvenanceCard({ surface }: { surface: PresentationSurface }) {
-
-
- View schema
-
- -
- {surface.view_schema}
-
-
-
-
- Generated at
+
+ {zh ? "发布时间" : "Published at"}
{surface.generated_at}
-
- Extension revision
+
+ {zh ? "原复核期限" : "Original review deadline"}
-
- {surface.detail_ref.extension_revision}
-
-
-
-
- Payload SHA-256
-
-
- {surface.detail_ref.payload_sha256}
+
+ {surface.review_due_at ?? unknown}
+
+
+ {zh ? "来源与引用详情" : "Source and reference details"}
+
+
+ {references.map(([label, value]) =>
+
- {label}
+ - {value ?? unknown}
+ )}
+
+
-
+ Payload SHA-256
+
+ -
+ {surface.detail_ref.payload_sha256}
+
+
+
+
);
@@ -318,12 +341,16 @@ export function DecisionResearchSurface({
view = null,
viewError = null,
viewLoading = false,
+ projection = null,
+ zh = false,
}: {
goals: RunGoal[];
surface: PresentationSurface;
view?: DecisionResearchView | null;
viewError?: string | null;
viewLoading?: boolean;
+ projection?: PresentationProjectionEnvelope | null;
+ zh?: boolean;
}) {
if (surface.state === "invalid") {
return (
@@ -416,7 +443,7 @@ export function DecisionResearchSurface({
Loading the full research view…
) : null}
-
+
>
) : (
<>
@@ -837,7 +864,7 @@ export function DecisionResearchSurface({
-
+
>
)}