diff --git a/src/application/investigation/EvidenceLedger.residualUnknown.test.ts b/src/application/investigation/EvidenceLedger.residualUnknown.test.ts index bb81f97ced..2885c1906f 100644 --- a/src/application/investigation/EvidenceLedger.residualUnknown.test.ts +++ b/src/application/investigation/EvidenceLedger.residualUnknown.test.ts @@ -78,6 +78,51 @@ const update = ( }); describe("residual unknown registry reads and identity", () => { + it("seals mutation Evidence without freezing caller-owned inputs", () => { + const store = ledger(); + const recordMutation = mutation("mutable-record-mutation"); + const recorded = store.recordUnknown( + input("Does record mutation evidence stay stable?"), + recordMutation, + ); + if (!recorded.ok) throw recorded.error; + + const updateMutation = mutation("mutable-update-mutation"); + const updated = store.updateUnknown(update(recorded.value), updateMutation); + if (!updated.ok) throw updated.error; + const atomicEvidence = evidence("mutable-atomic-evidence"); + const atomicMutation = mutation("mutable-atomic-mutation"); + const atomicallyRecorded = store.recordWithUnknown( + atomicEvidence, + input("Does atomic evidence stay stable?"), + atomicMutation, + ); + if (!atomicallyRecorded.ok) throw atomicallyRecorded.error; + + for (const callerEvidence of [ + recordMutation, + updateMutation, + atomicEvidence, + atomicMutation, + ]) { + expect(Object.isFrozen(callerEvidence)).toBe(false); + const retained = store.forAnalysis(callerEvidence.evidence_id); + if (retained === undefined) throw new Error("Missing retained Evidence"); + const originalResult = structuredClone(retained.normalized_result); + expect(Object.isFrozen(retained)).toBe(true); + Reflect.set(callerEvidence, "operation", "caller mutation"); + expect( + Reflect.set( + callerEvidence.normalized_result as object, + "label", + "caller nested mutation", + ), + ).toBe(true); + expect(retained.operation).not.toBe("caller mutation"); + expect(retained.normalized_result).toEqual(originalResult); + } + }); + it("merges record-only bundles without changing retained unknown history", () => { const store = ledger(); const created = store.recordUnknown( diff --git a/src/application/investigation/EvidenceLedger.ts b/src/application/investigation/EvidenceLedger.ts index ce806ad38a..0cb1c5e296 100644 --- a/src/application/investigation/EvidenceLedger.ts +++ b/src/application/investigation/EvidenceLedger.ts @@ -11,11 +11,7 @@ import { import { AnalysisInputError } from "../../domain/analysisErrorCore.js"; import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js"; import { UnknownRegistryError } from "../../domain/unknownRegistryError.js"; -import { - immutableEvidence, - parseEvidence, - type Evidence, -} from "../../domain/evidence.js"; +import { immutableEvidence, type Evidence } from "../../domain/evidence.js"; import { createResidualUnknown, updateResidualUnknown, @@ -220,7 +216,7 @@ export class EvidenceLedger { ): Result { let parsedMutation: Evidence; try { - parsedMutation = parseEvidence(mutationEvidence); + parsedMutation = immutableEvidence(mutationEvidence); } catch (cause: unknown) { return err( new EvidenceIntegrityError("Unknown mutation evidence is invalid", { @@ -255,8 +251,8 @@ export class EvidenceLedger { let evidence: Evidence; let mutation: Evidence; try { - evidence = parseEvidence(evidenceInput); - mutation = parseEvidence(mutationInput); + evidence = immutableEvidence(evidenceInput); + mutation = immutableEvidence(mutationInput); } catch (cause: unknown) { return err( new EvidenceIntegrityError("Atomic Evidence validation failed", { @@ -394,7 +390,7 @@ export class EvidenceLedger { ): Result { let mutationEvidence: Evidence; try { - mutationEvidence = parseEvidence(mutationEvidenceInput); + mutationEvidence = immutableEvidence(mutationEvidenceInput); } catch (cause: unknown) { return err( new EvidenceIntegrityError("Unknown mutation evidence is invalid", { diff --git a/tests/boundary/mcp/serverUnknownsMcp.test.ts b/tests/boundary/mcp/serverUnknownsMcp.test.ts index 0a27324ec5..e39ce2aecc 100644 --- a/tests/boundary/mcp/serverUnknownsMcp.test.ts +++ b/tests/boundary/mcp/serverUnknownsMcp.test.ts @@ -281,7 +281,7 @@ it("pages filtered unknown heads through the public MCP contract", async () => { supporting_evidence_ids: [], contradicting_evidence_ids: [], }); - expect(result.ok).toBe(true); + if (!result.ok) throw result.error; } const defaultPage = structured( @@ -372,12 +372,11 @@ it("pages filtered unknown heads through the public MCP contract", async () => { .result as typeof defaultPage; expect(first).toMatchObject({ total: 3, offset: 0, next_offset: 2 }); expect(first.items).toHaveLength(2); - const second = structured( - await client.callTool({ - ...request, - arguments: { ...request.arguments, offset: first.next_offset ?? 0 }, - }), - ).result as typeof defaultPage; + const secondResponse = await client.callTool({ + ...request, + arguments: { ...request.arguments, offset: first.next_offset ?? 0 }, + }); + const second = structured(secondResponse).result as typeof defaultPage; expect(second).toMatchObject({ total: 3, offset: 2, next_offset: null }); expect(second.items).toHaveLength(1); expect( @@ -389,7 +388,7 @@ it("pages filtered unknown heads through the public MCP contract", async () => { ); expect( toolContract("list_unknowns").outputSchema.safeParse( - (await client.callTool(request)).structuredContent, + secondResponse.structuredContent, ).success, ).toBe(true);