From 07ea504e91b5c69d87828c7cad37f0f2ef0b9c6c Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Mon, 10 Aug 2026 17:18:52 +0800 Subject: [PATCH] fix: align report technique audit evidence --- .../src/lib/personal-report-generation.ts | 59 +++++++++++-------- .../tests/personal-report-generation.test.ts | 34 +++++++++++ 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/frontend/src/lib/personal-report-generation.ts b/frontend/src/lib/personal-report-generation.ts index 7fc3e1e4..ff6d0ffc 100644 --- a/frontend/src/lib/personal-report-generation.ts +++ b/frontend/src/lib/personal-report-generation.ts @@ -426,7 +426,7 @@ function sectionEvidenceStatus( name: string, sourcePath: string, ): "verified" | "partial" | "blocked" { - if (status === "missing" || status === "blocked") return "blocked"; + if (status === "missing" || status === "blocked" || status === "official_blocked") return "blocked"; if (status === "verified") return "verified"; if (status === "partial") return "partial"; // used / available / received_unverified / unknown / undefined @@ -527,7 +527,6 @@ export function buildReportEvidencePacket(input: BuildEvidencePacketInput): Repo const techniqueAudit: { technique: string; status: string; note: string }[] = []; const seenTechniques = new Set(); const sections = readSections(machinePacket); - const sectionsByName = new Map(sections.map((section) => [section.name, section])); // Layer names alone are route availability, not verified facts: they map to // partial at best. for (const technique of [...availableLayers, ...missingLayers, ...hardBlockers]) { @@ -548,22 +547,30 @@ export function buildReportEvidencePacket(input: BuildEvidencePacketInput): Repo : "", }); } - // Machine-packet sections (object map in the real engine, array accepted). - // The section name is the technique key; status comes from section.status - // via the deterministic sectionEvidenceStatus rule. + // Machine-packet facts override route-layer availability for the same technique. + const sectionReason: Readonly> = { + missing: "未返回本次计算数据", + official_blocked: "官方外部引擎本次未验证或不可用", + received_unverified: "已收到外部响应,但未完成官方验证", + local_fallback: "本次仅有本地回退结果,未取得官方验证", + blocked: "本次执行被阻塞", + }; for (const section of sections) { - if (seenTechniques.has(section.name)) continue; - seenTechniques.add(section.name); - techniqueAudit.push({ + const row = { technique: section.name, - status: section.status, - note: section.sourcePath ? `source: ${section.sourcePath}` : "", - }); + status: sectionEvidenceStatus(section.status, section.name, section.sourcePath), + note: [sectionReason[section.status], section.sourcePath ? `source: ${section.sourcePath}` : ""] + .filter(Boolean) + .join(";"), + }; + const existingIndex = techniqueAudit.findIndex((item) => item.technique === section.name); + if (existingIndex >= 0) techniqueAudit[existingIndex] = row; + else techniqueAudit.push(row); } const blockedTechniques = hardBlockers.length > 0 ? hardBlockers - : techniqueAudit.filter((row) => row.status === "blocked").map((row) => row.technique); + : sections.filter((section) => section.status === "blocked").map((section) => section.name); const conflicts: { techniques: string[]; summary: string }[] = []; const rawConflicts = Array.isArray(machinePacket.conflicts) @@ -580,14 +587,10 @@ export function buildReportEvidencePacket(input: BuildEvidencePacketInput): Repo const evidenceRefs: { id: string; technique: string; status: EvidenceRefStatus }[] = []; techniqueAudit.forEach((row, index) => { - const section = sectionsByName.get(row.technique); - const status = section - ? sectionEvidenceStatus(section.status, section.name, section.sourcePath) - : canonicalTechniqueStatus(row.status); evidenceRefs.push({ id: `ev-audit-${index + 1}`, technique: row.technique, - status, + status: canonicalTechniqueStatus(row.status), }); }); conflicts.forEach((conflict, index) => { @@ -713,15 +716,21 @@ export function assembleReportDocument( throw new ReportEvidenceInsufficientError("ascendant_missing"); } + const usedEvidenceRefs = new Set( + input.agentOutput.thematicNarrative.flatMap((section) => section.evidenceRefs), + ); const techniqueAudit: EvidenceAppendix["techniqueAudit"] = packet.techniqueAudit.map( - (row, index) => ({ - id: `ev-audit-${index + 1}`, - techniqueId: techniqueSlug(row.technique, `tech-${index + 1}`), - techniqueName: row.technique, - status: canonicalTechniqueStatus(row.status), - used: canonicalTechniqueStatus(row.status) === "verified", - ...(row.note ? { notes: row.note.slice(0, 500) } : {}), - }), + (row, index) => { + const id = `ev-audit-${index + 1}`; + return { + id, + techniqueId: techniqueSlug(row.technique, `tech-${index + 1}`), + techniqueName: row.technique, + status: canonicalTechniqueStatus(row.status), + used: usedEvidenceRefs.has(id), + ...(row.note ? { notes: row.note.slice(0, 500) } : {}), + }; + }, ); const conflicts: EvidenceAppendix["conflicts"] = packet.conflicts.map((conflict, index) => ({ diff --git a/frontend/tests/personal-report-generation.test.ts b/frontend/tests/personal-report-generation.test.ts index cff3bd82..4dbdf950 100644 --- a/frontend/tests/personal-report-generation.test.ts +++ b/frontend/tests/personal-report-generation.test.ts @@ -452,6 +452,40 @@ test("assembled document passes the canonical server parse with a D1 of 12 house ); }); +test("technique audit reflects final evidence status and narrative usage", () => { + const workflow = pythonStyleChartPayload() as Record; + const machinePacket = workflow.machine_evidence_packet as Record; + const sections = machinePacket.sections as Record>; + sections.external_oracle_status.status = "official_blocked"; + const packet = buildPacket({ workflow }); + assert.deepEqual(packet.blockedTechniques, []); + const output = agentOutput(); + const blockedRef = packet.evidenceRefs.find((ref) => ref.technique === "D2"); + assert.ok(blockedRef); + output.thematicNarrative[0].evidenceRefs.push(blockedRef.id); + const document = assembleReportDocument({ + reportId: "22222222-2222-4222-8222-222222222222", + generatedAt: "2026-08-06T00:00:00.000Z", + packet, + agentOutput: output, + }); + const auditById = new Map(document.evidenceAppendix.techniqueAudit.map((row) => [row.id, row])); + const referencedIds = new Set(document.thematicNarrative.flatMap((section) => section.evidenceRefs)); + + assert.ok([...referencedIds].every((id) => auditById.get(id)?.used === true)); + assert.equal(document.evidenceAppendix.techniqueAudit.find((row) => row.techniqueName === "D1")?.status, "verified"); + assert.equal(document.evidenceAppendix.techniqueAudit.find((row) => row.techniqueName === "D2")?.status, "blocked"); + assert.match( + document.evidenceAppendix.techniqueAudit.find((row) => row.techniqueName === "D2")?.notes ?? "", + /未返回本次计算数据/, + ); + const external = document.evidenceAppendix.techniqueAudit.find( + (row) => row.techniqueName === "external_oracle_status", + ); + assert.equal(external?.status, "blocked"); + assert.match(external?.notes ?? "", /官方外部引擎本次未验证或不可用/); +}); + test("D9/D10 charts are omitted when no real divisional houses exist", () => { const workflow = pythonStyleChartPayload() as Record; const chart = workflow.chart as Record;