From 4b6fd031467d7560e0311b29f94ab97e9869fd08 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:57:31 +0800 Subject: [PATCH] refactor(evidence): consolidate import staging --- .../EvidenceLedger.residualUnknown.test.ts | 18 ++++- .../investigation/EvidenceLedger.ts | 73 ++++++++----------- 2 files changed, 47 insertions(+), 44 deletions(-) diff --git a/src/application/investigation/EvidenceLedger.residualUnknown.test.ts b/src/application/investigation/EvidenceLedger.residualUnknown.test.ts index 229b868044..efed5ceb10 100644 --- a/src/application/investigation/EvidenceLedger.residualUnknown.test.ts +++ b/src/application/investigation/EvidenceLedger.residualUnknown.test.ts @@ -4,7 +4,10 @@ import canonicalize from "canonicalize"; import { describe, expect, it } from "vitest"; import { EvidenceLedger } from "./EvidenceLedger.js"; -import { createEvidence } from "../../domain/evidence.js"; +import { + createEvidence, + withEvidenceCallerInputPath, +} from "../../domain/evidence.js"; import { createEvidenceBundle, parseEvidenceBundle, @@ -405,8 +408,20 @@ describe("residual unknown registry resolution and history", () => { const base = ledger(); const created = base.recordUnknown(input("Divergent?"), mutation("base")); if (!created.ok) throw created.error; + const source = withEvidenceCallerInputPath( + evidence("shared-source"), + "./original", + ); + expect(base.record(source).ok).toBe(true); const branch = ledger(); expect(branch.import(base.export()).ok).toBe(true); + expect( + branch.import( + createEvidenceBundle([ + withEvidenceCallerInputPath(source, "./relocated"), + ]), + ).ok, + ).toBe(true); expect( base.updateUnknown(update(created.value), mutation("branch-left")).ok, ).toBe(true); @@ -418,6 +433,7 @@ describe("residual unknown registry resolution and history", () => { ).toBe(true); const before = serializeEvidenceBundle(base.export()); expect(base.import(branch.export()).ok).toBe(false); + expect(base.get(source.evidence_id)?.caller_input_path).toBe("./original"); expect(serializeEvidenceBundle(base.export())).toBe(before); }); }); diff --git a/src/application/investigation/EvidenceLedger.ts b/src/application/investigation/EvidenceLedger.ts index 3b3bb4cfc7..f4747b6b62 100644 --- a/src/application/investigation/EvidenceLedger.ts +++ b/src/application/investigation/EvidenceLedger.ts @@ -100,55 +100,43 @@ export class EvidenceLedger { ); } const bundle = parsed.value; - // A record-only bundle cannot change the already-valid unknown graph. The - // bundle parser has validated every incoming record and its manifests, so - // checking ID conflicts and committing only the additions preserves the - // atomic merge without rebuilding and reparsing the entire ledger. - if (bundle.unknowns.length === 0) { - const additions = new Map(); - const replacements: EvidenceMetadataReplacement[] = []; - for (const evidence of bundle.records) { - const existing = this.#records.get(evidence.evidence_id); - if (existing !== undefined) { - if (!recordsAgree(existing, evidence)) - return err( - new EvidenceIntegrityError("Conflicting evidence record"), - ); - if (!displayMetadataAgree(existing, evidence)) { - replacements.push({ - kind: "metadata", - previous: existing, - current: evidence, - }); - } - continue; - } - additions.set(evidence.evidence_id, evidence); - } - for (const { current } of replacements) - this.#records.set(current.evidence_id, current); - for (const [id, evidence] of additions) this.#records.set(id, evidence); - return ok({ - recordsAdded: additions.size, - unknownsAdded: 0, - changed: additions.size > 0 || replacements.length > 0, - replacements, - }); - } - const pending = new Map(this.#records); + // Stage record conflicts and display metadata once, before either commit + // path. Unknown history additionally requires validating the merged graph. + const recordChanges = new Map(); + let recordsAdded = 0; const replacements: EvidenceMetadataReplacement[] = []; for (const evidence of bundle.records) { - const existing = pending.get(evidence.evidence_id); - if (existing !== undefined && !recordsAgree(existing, evidence)) + const existing = this.#records.get(evidence.evidence_id); + if (existing === undefined) { + recordChanges.set(evidence.evidence_id, evidence); + recordsAdded += 1; + continue; + } + if (!recordsAgree(existing, evidence)) return err(new EvidenceIntegrityError("Conflicting evidence record")); - if (existing !== undefined && !displayMetadataAgree(existing, evidence)) + if (!displayMetadataAgree(existing, evidence)) { + recordChanges.set(evidence.evidence_id, evidence); replacements.push({ kind: "metadata", previous: existing, current: evidence, }); - pending.set(evidence.evidence_id, evidence); + } } + // A record-only bundle cannot change the already-valid unknown graph. + if (bundle.unknowns.length === 0) { + for (const evidence of recordChanges.values()) + this.#records.set(evidence.evidence_id, evidence); + return ok({ + recordsAdded, + unknownsAdded: 0, + changed: recordChanges.size > 0, + replacements, + }); + } + const pending = new Map(this.#records); + for (const evidence of recordChanges.values()) + pending.set(evidence.evidence_id, evidence); const pendingUnknowns = new Map(this.#unknownRevisions); let unknownsAdded = 0; for (const unknown of bundle.unknowns) { @@ -170,12 +158,11 @@ export class EvidenceLedger { }), ); } - const added = pending.size - this.#records.size; this.#commit(checked.value); return ok({ - recordsAdded: added, + recordsAdded, unknownsAdded, - changed: added > 0 || unknownsAdded > 0 || replacements.length > 0, + changed: recordChanges.size > 0 || unknownsAdded > 0, replacements, }); }