fix: align report technique audit evidence
This commit is contained in:
@@ -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<string>();
|
||||
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<Record<string, string>> = {
|
||||
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) => ({
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
const machinePacket = workflow.machine_evidence_packet as Record<string, unknown>;
|
||||
const sections = machinePacket.sections as Record<string, Record<string, unknown>>;
|
||||
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<string, unknown>;
|
||||
const chart = workflow.chart as Record<string, unknown>;
|
||||
|
||||
Reference in New Issue
Block a user