From 470c9910d05ae7079aa9227508a6a9a45969edb5 Mon Sep 17 00:00:00 2001 From: Yuchao Wang Date: Tue, 3 Nov 2026 07:05:29 +0000 Subject: [PATCH] feat(research): download readable original-evidence reports Signed-off-by: Yuchao Wang --- .../research-record-browser.tsx | 13 +++- .../research-record-export.ts | 14 +++-- .../research-record-report.test.mjs | 54 ++++++++++++++++ .../research-record-report.ts | 58 ++++++++++++++++++ docs/images/research-record-report.png | Bin 0 -> 45234 bytes .../research-history.md | 25 +++++++- .../research-record-search.mjs | 41 +++++++++++++ 7 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/research-record-report.test.mjs create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/research-record-report.ts create mode 100644 docs/images/research-record-report.png diff --git a/apps/presentation/dashboard/src/features/personal-workspace/research-record-browser.tsx b/apps/presentation/dashboard/src/features/personal-workspace/research-record-browser.tsx index 9c6c3f88fb..1b82d8618e 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/research-record-browser.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/research-record-browser.tsx @@ -5,6 +5,7 @@ import {useMemo, useState, type ReactNode} from "react"; import type {ExploreHistoryEvent} from "../../../../../../loopx/control_plane/presentation/explore_history.ts"; import {searchResearchRecords, type ResearchRecordDisplay} from "./research-record-search"; import {exportResearchRecords} from "./research-record-export"; +import {exportResearchReport} from "./research-record-report"; import {downloadReport} from "./report-download"; /** Local read controls over the existing validated history snapshot. */ @@ -20,6 +21,7 @@ export function ResearchRecordSearch({goalId, events, displayEvents, displaySect const [status, setStatus] = useState(""); const [limit, setLimit] = useState(50); const [downloadFailed, setDownloadFailed] = useState(false); + const [format, setFormat] = useState("json"); const displayed = useMemo(() => new Map(displayEvents?.map(event => [event.event_id, event])), [displayEvents]); const display = useMemo(() => ({events: displayed, sections: displaySections}), [displayed, displaySections]); const matches = useMemo(() => searchResearchRecords(events, query, status, display), [events, query, status, display]); @@ -38,13 +40,18 @@ export function ResearchRecordSearch({goalId, events, displayEvents, displaySect {[...new Set([...statuses, ...(status ? [status] : [])])].map(item => )} {query || status ? : null} + + }}>{format === "md" ? (zh ? "下载阅读报告" : "Download reading report") : (zh ? "下载匹配记录(JSON)" : "Download matches (JSON)")} {downloadFailed ?

{zh ? "无法开始下载,记录未改变。请重试下载。" : "Could not start the download. Records are unchanged; try downloading again."}

: null}
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/research-record-export.ts b/apps/presentation/dashboard/src/features/personal-workspace/research-record-export.ts index 16ab0ab832..fff0532b21 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/research-record-export.ts +++ b/apps/presentation/dashboard/src/features/personal-workspace/research-record-export.ts @@ -5,13 +5,15 @@ import {searchResearchRecords, type ResearchRecordDisplay} from "./research-reco * Export canonical records, never translated/analysed display replacements. * Source endpoints are deliberately absent: opening a file needs no connection. */ -export function exportResearchRecords({goalId, events, query, status, complete, stale, exportedAt, display}: { +export type ResearchRecordExport = { goalId: string; events: readonly ExploreHistoryEvent[]; query: string; status: string; complete: boolean; stale: boolean; exportedAt: string; display?: ResearchRecordDisplay; -}): string { +}; + +export function researchRecordSnapshot({goalId, events, query, status, complete, stale, exportedAt, display}: ResearchRecordExport) { if (!goalId || events.some(event => event.goal_id !== goalId)) throw new Error("Research export Goal mismatch"); const records = searchResearchRecords(events, query, status, display); - return JSON.stringify({ + return { goal_id: goalId, exported_at: exportedAt, filters: {query, status}, @@ -22,5 +24,9 @@ export function exportResearchRecords({goalId, events, query, status, complete, refresh_failed: stale, }, records, - }, null, 2) + "\n"; + }; +} + +export function exportResearchRecords(input: ResearchRecordExport): string { + return JSON.stringify(researchRecordSnapshot(input), null, 2) + "\n"; } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/research-record-report.test.mjs b/apps/presentation/dashboard/src/features/personal-workspace/research-record-report.test.mjs new file mode 100644 index 0000000000..417c007702 --- /dev/null +++ b/apps/presentation/dashboard/src/features/personal-workspace/research-record-report.test.mjs @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import {test} from "node:test"; +import {exportResearchReport} from "./research-record-report.ts"; + +const record = (overrides = {}) => ({goal_id: "research", title: "Evidence", summary: "Original result", + event_id: "event", result_id: "finding", recorded_at: "2026-01-01T00:00:00Z", status: "refuted", + evidence_refs: [], confidence: null, ...overrides}); +const input = (events, overrides = {}) => ({goalId: "research", events, query: "", status: "", + complete: true, stale: false, exportedAt: "2026-01-02T00:00:00Z", ...overrides}); + +test("report preserves all matching originals beyond UI paging and the actual read scope", () => { + const events = Object.freeze(Array.from({length: 75}, (_, i) => Object.freeze(record({title: `Observation ${i}`, event_id: `event-${i}`})))); + const report = exportResearchReport(input(events, {complete: false, stale: true, query: "Observation", status: "refuted"}), false); + assert.equal((report.match(/^## \d+\./gm) ?? []).length, 75); + assert.match(report, /75 \/ 75/); + assert.match(report, /Incomplete; unloaded records are not included/); + assert.match(report, /Failed; previously read records retained/); + assert.match(report, /\*\*Search:\*\* Observation/); + assert.match(report, /\*\*Status filter:\*\* refuted/); + assert.match(report, /Observation 74/); +}); + +test("translated matches keep original prose, negative revisions and measurement conditions", () => { + const old = record({summary: "Earlier negative evidence", evidence_refs: ["result:old"], run_id: "run:old", + measurements: [{metric: "Error", value: null, unit: "points", higher_is_better: false, + comparison_scope: "scope:v1", input_revision: "input:v1", evaluation_ref: "method:v1", candidate_ref: "candidate:v1", evidence_ref: "result:old"}]}); + const later = record({event_id: "later", summary: "Measured zero", supersedes: "event", + measurements: [{...old.measurements[0], value: 0}]}); + const display = {events: new Map([[old.event_id, {...old, summary: "较早的负向证据"}]])}; + const report = exportResearchReport(input([old, later], {query: "负向证据", display}), true); + assert.match(report, /Earlier negative evidence/); + assert.doesNotMatch(report, /Measured zero|较早的负向证据/); + assert.match(report, /\*\*数值:\*\* 未知/); + for (const ref of ["scope:v1", "input:v1", "method:v1", "candidate:v1", "result:old", "run:old"]) assert(report.includes(ref)); + const both = exportResearchReport(input([old, later]), false); + assert.match(both, /\*\*Value:\*\* Unknown/); + assert.match(both, /\*\*Value:\*\* 0/); + assert.match(both, /\*\*Direction:\*\* Lower is better/); + assert.match(both, /\*\*supersedes:\*\* event/); +}); + +test("source markup remains literal without loading remote images or forging report sections", () => { + const summary = "```\n# Forged claim\n![remote](https://example.test/image)\n\n````\n结论"; + const report = exportResearchReport(input([record({title: "\n# title", summary, + evidence_refs: ["[source](https://example.test)"], execution_observation: {basis: "observed"}})]), false); + assert(report.includes(`\`\`\`\`\`text\n${summary}\n\`\`\`\`\``)); + assert.match(report, /## 1\. \\ \\# title/); + assert(report.includes('"basis": "observed"')); +}); + +test("empty reports keep scope and mixed Goals are rejected before filtering", () => { + assert.match(exportResearchReport(input([]), false), /0 \/ 0/); + assert.throws(() => exportResearchReport(input([record({goal_id: "other"})], {query: "absent"}), false), /Goal mismatch/); +}); diff --git a/apps/presentation/dashboard/src/features/personal-workspace/research-record-report.ts b/apps/presentation/dashboard/src/features/personal-workspace/research-record-report.ts new file mode 100644 index 0000000000..6536e1de8e --- /dev/null +++ b/apps/presentation/dashboard/src/features/personal-workspace/research-record-report.ts @@ -0,0 +1,58 @@ +import {researchRecordSnapshot, type ResearchRecordExport} from "./research-record-export.ts"; + +// Source prose stays literal: it cannot introduce headings, remote images or +// links into the saved report. Fences grow to preserve embedded code fences. +function literal(text: string): string { + const fence = "`".repeat(Math.max(3, ...Array.from(text.matchAll(/`+/g), match => match[0].length + 1))); + return `${fence}text\n${text}\n${fence}`; +} +const inline = (text: string) => text.replace(/[\\`*_{}\[\]<>()#+.!|~>-]/g, "\\$&").replace(/\r?\n/g, " "); + +/** Human-readable original evidence, using exactly the JSON export's selection. */ +export function exportResearchReport(input: ResearchRecordExport, zh: boolean): string { + const snapshot = researchRecordSnapshot(input); + const label = (en: string, cn: string) => zh ? cn : en; + const missing = label("Not recorded", "未记录"); + const field = (name: string, value: unknown) => `- **${name}:** ${inline(value === undefined || value === null || value === "" ? missing : String(value))}`; + const lines = [ + `# ${label("Research record report", "研究记录报告")}`, + label("Original recorded evidence; not an analysis, matched evaluation or acceptance receipt.", "原始记录证据;不是分析、同条件评测或验收回执。"), + field("Goal", snapshot.goal_id), field(label("Exported at", "导出时间"), snapshot.exported_at), + `## ${label("Reading scope", "阅读范围")}`, + field(label("Exported / loaded records", "导出 / 已读记录"), `${snapshot.coverage.exported_records} / ${snapshot.coverage.loaded_records}`), + field(label("History", "历史"), input.complete ? label("Complete loaded history", "已完整载入历史") : label("Incomplete; unloaded records are not included", "不完整;不包含尚未读取的记录")), + field(label("Refresh", "刷新"), input.stale ? label("Failed; previously read records retained", "失败;保留先前读取的记录") : label("No refresh failure reported", "未报告刷新失败")), + field(label("Search", "搜索"), input.query || label("All text", "全部文字")), + field(label("Status filter", "状态筛选"), input.status || label("All statuses", "全部状态")), + label("Filters may match saved display translations; the records below retain their original language and revision. Record time is not execution time. References are identifiers, not downloaded source files. Keep this report as private as its source research.", + "筛选可能匹配已保存的展示翻译;以下记录保留原始语言与版本。记录时间不是执行时间。引用是标识,不包含其源文件。请按原研究的隐私范围保管本报告。"), + ]; + snapshot.records.forEach((record, index) => { + lines.push(`## ${index + 1}. ${inline(record.title)}`, field(label("Recorded at", "记录时间"), record.recorded_at), + field(label("Recorded status", "记录状态"), record.status), + `### ${label("Original text", "原文")}`, record.summary ? literal(record.summary) : missing); + if (record.blocked_reason) lines.push(`### ${label("Blocked reason", "受阻原因")}`, literal(record.blocked_reason)); + if (record.measurements?.length) { + lines.push(`### ${label("Native measurements", "原生测量")}`); + for (const measurement of record.measurements) { + lines.push(`#### ${inline(measurement.metric)}`, + field(label("Value", "数值"), measurement.value === null ? label("Unknown", "未知") : measurement.value), + field(label("Unit", "单位"), measurement.unit), + field(label("Direction", "方向"), measurement.higher_is_better ? label("Higher is better", "越高越好") : label("Lower is better", "越低越好")), + field(label("Comparison scope", "比较范围"), measurement.comparison_scope), + field(label("Input revision", "输入版本"), measurement.input_revision), + field(label("Evaluation reference", "评测引用"), measurement.evaluation_ref), + field(label("Candidate reference", "候选引用"), measurement.candidate_ref), + field(label("Evidence reference", "证据引用"), measurement.evidence_ref)); + } + } + lines.push(`### ${label("Sources and identity", "来源与标识")}`); + for (const key of ["schema_version", "event_id", "event_kind", "result_id", "node_kind", "node_id", "parent_id", "from_node", "to_node", "edge_type", "agent_id", "run_id", "supersedes"] as const) { + if (record[key]) lines.push(field(key, record[key])); + } + lines.push(field(label("Recorded confidence", "记录置信度"), record.confidence)); + lines.push(record.evidence_refs.length ? literal(record.evidence_refs.join("\n")) : label("No evidence references recorded.", "未记录证据引用。")); + if (record.execution_observation) lines.push(`### ${label("Recorded execution observation", "记录的执行观察")}`, literal(JSON.stringify(record.execution_observation, null, 2))); + }); + return lines.join("\n\n") + "\n"; +} diff --git a/docs/images/research-record-report.png b/docs/images/research-record-report.png new file mode 100644 index 0000000000000000000000000000000000000000..b12f410675dc9369559da5a1e141b25d996de8e2 GIT binary patch literal 45234 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 { + await results.getByRole("combobox", {name: "Download format"}).selectOption("md"); + const received = page.waitForEvent("download"); + await results.getByRole("button", {name: "Download reading report", exact: true}).click(); + const file = await received; + assert.match(file.suggestedFilename(), /-readback\.md$/); + const text = await readFile(await file.path(), "utf8"); + await results.getByRole("combobox", {name: "Download format"}).selectOption("json"); + return text; + }; await search.waitFor(); assert(await search.evaluate(element => element === document.activeElement), "The search entry moves keyboard focus to the input"); await page.waitForFunction(() => document.querySelector('.research-history')?.getAttribute('aria-busy') === 'false'); @@ -102,6 +112,11 @@ Path(sys.argv[2]).write_text(json.dumps({'goal_id':other,'display_name':'Other s assert.equal(all.goal_id, "synthetic-record-search"); assert.equal(all.records.length, 79, "Download includes matches beyond both HTTP and display paging"); assert.deepEqual(all.coverage, {loaded_records: 79, exported_records: 79, history_complete: true, refresh_failed: false}); + const fullReport = await report(); + assert.equal((fullReport.match(/^## \d+\./gm) ?? []).length, 79); + assert.match(fullReport, /\*\*Value:\*\* Unknown/); + assert.match(fullReport, /\*\*Value:\*\* 0/); + assert.match(fullReport, /candidate:amber-v3/); await search.fill("AMBER-V1 run:old"); await results.getByRole("status", {name: /^1 matches in 79 loaded records/}).waitFor(); assert.equal(await results.locator("li").count(), 1, "Search reaches beyond both the display limit and HTTP page boundary"); @@ -127,6 +142,10 @@ Path(sys.argv[2]).write_text(json.dumps({'goal_id':other,'display_name':'Other s assert.equal(selected.records[0].summary, "质量下降 — original negative evidence for candidate amber."); assert.deepEqual(selected.records[0].evidence_refs, ["candidate:amber-v1"]); assert.equal(selected.records[0].run_id, "run:old"); + const selectedReport = await report(); + assert.match(selectedReport, /质量下降 — original negative evidence/); + assert.match(selectedReport, /\*\*Comparison scope:\*\* synthetic:fixed/); + assert.doesNotMatch(selectedReport, /candidate:amber-v2|Later observation/); // Compare the original negative record with a revision excluded by both filters. const comparison = results.locator(".research-record-comparison"); const comparisonSummary = comparison.locator(":scope > summary"); @@ -180,6 +199,16 @@ Path(sys.argv[2]).write_text(json.dumps({'goal_id':other,'display_name':'Other s await page.screenshot({path: resolve(outputDir, "research-record-export-download-error.png")}); assert.deepEqual((await download()).records, selected.records); assert.equal(await results.getByRole("alert").count(), 0); + await results.getByRole("combobox", {name: "Download format"}).selectOption("md"); + await page.evaluate(() => { + const original = URL.createObjectURL; + URL.createObjectURL = () => {URL.createObjectURL = original; throw new Error("Synthetic report interruption");}; + }); + await results.getByRole("button", {name: "Download reading report", exact: true}).click(); + await results.getByRole("alert").waitFor(); + assert.match(await report(), /Earlier negative observation/); + assert.equal(await results.getByRole("alert").count(), 0); + await results.getByRole("combobox", {name: "Download format"}).selectOption("md"); for (const width of [1440, 390]) { await page.setViewportSize({width, height: 900}); await results.evaluate(element => element.scrollIntoView({block: "start"})); @@ -187,6 +216,7 @@ Path(sys.argv[2]).write_text(json.dumps({'goal_id':other,'display_name':'Other s await page.screenshot({path: resolve(outputDir, `research-record-search-${width}.png`), animations: "disabled"}); } // A failed refresh keeps the selected query and old evidence visibly stale. + await results.getByRole("combobox", {name: "Download format"}).selectOption("json"); await page.route("**/api/chat/explore-results?*", route => route.fulfill({status: 503, json: {error: "Synthetic interruption"}})); await history.getByRole("button", {name: "Refresh history", exact: true}).click(); await results.getByRole("status", {name: /^1 matches in 79 loaded records/}).getByText(/Refresh failed/).waitFor(); @@ -194,6 +224,7 @@ Path(sys.argv[2]).write_text(json.dumps({'goal_id':other,'display_name':'Other s assert.equal(await results.locator("li").count(), 1); const stale = await download(); assert.equal(stale.coverage.refresh_failed, true); + assert.match(await report(), /Failed; previously read records retained/); await comparisonSummary.click(); await comparison.getByRole("combobox", {name: "Compare with"}).waitFor(); assert.match(await comparison.innerText(), /candidate:amber-v2/); @@ -227,6 +258,7 @@ Path(sys.argv[2]).write_text(json.dumps({'goal_id':other,'display_name':'Other s const partial = await download(); assert.equal(partial.coverage.history_complete, false); assert.equal(partial.coverage.refresh_failed, true); + assert.match(await report(), /Incomplete; unloaded records are not included/); assert.equal(partial.records.length, partial.coverage.loaded_records); assert(partial.records.length > 0 && partial.records.length < 79); await search.fill("amber-v1"); @@ -285,6 +317,15 @@ Path(sys.argv[2]).write_text(json.dumps({'goal_id':other,'display_name':'Other s await chineseDownload.focus(); await page.keyboard.press("Enter"); assert.equal(JSON.parse(await readFile(await (await received).path(), "utf8")).records.length, 79); + await results.getByRole("combobox", {name: "下载格式"}).selectOption("md"); + const chineseReport = page.waitForEvent("download"); + await results.getByRole("button", {name: "下载阅读报告", exact: true}).focus(); + await page.keyboard.press("Enter"); + const reportText = await readFile(await (await chineseReport).path(), "utf8"); + assert.match(reportText, /^# 研究记录报告/); + assert.match(reportText, /\*\*数值:\*\* 未知/); + assert.match(reportText, /\*\*数值:\*\* 0/); + assert.match(reportText, /质量下降 — original negative evidence/); return {coverageEntries: collectCoverage ? await page.coverage.stopJSCoverage() : [], note: "packaged evidence search/export through native paged history; full and filtered downloads, original revisions, coverage/staleness, original revision comparison, download/read recovery, Goal isolation and narrow viewport"}; } catch (error) { if (page && !page.isClosed()) await page.screenshot({path: resolve(outputDir, "research-record-search-failed.png")});