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({ - + )}