diff --git a/docs/reconstruction-coverage.md b/docs/reconstruction-coverage.md index f2f184ad14..a3cc64a63a 100644 --- a/docs/reconstruction-coverage.md +++ b/docs/reconstruction-coverage.md @@ -28,6 +28,11 @@ this effective scope. A claim verifier must commit to its own surfaces and their reachable dependencies, including their artifact and implemented-owner digests. Callers do not need to duplicate the transitive closure in the request. +Listed `residual_unknown_ids` select unknown identities from the Evidence bundle. +The evaluator uses each identity's latest revision: resolved questions do not +block the boundary, while reopened questions do. An active, unallowed question +retains its cited supporting, contradicting and mutation Evidence in the result. + ## Comparable verifier Evidence At least one record explicitly cited by the latest verifier observation must diff --git a/src/domain/reconstructionCoverageEvaluation.ts b/src/domain/reconstructionCoverageEvaluation.ts index dfd0c4063b..eca7c2d835 100644 --- a/src/domain/reconstructionCoverageEvaluation.ts +++ b/src/domain/reconstructionCoverageEvaluation.ts @@ -1,3 +1,4 @@ +import { reconstructionUnknownHeads } from "./reconstructionUnknowns.js"; import type { ReconstructionCoverageScope } from "./reconstructionCoverageScope.js"; import type { ReconstructionClosureResult, @@ -396,14 +397,27 @@ const verifierResultMatchesContract = ( export const evaluateReconstructionCoverageRisks = ( context: ReconstructionEvaluationContext, ): void => { - for (const unknownId of context.coverage.residual_unknown_ids) - if (!context.boundary.allowed_unknown_ids.includes(unknownId)) + const unknownHeads = reconstructionUnknownHeads( + context.coverage.evidence_bundle.unknowns, + ); + for (const unknownId of new Set(context.coverage.residual_unknown_ids)) { + const unknown = unknownHeads.get(unknownId); + if (unknown?.status === "resolved") continue; + if (!context.boundary.allowed_unknown_ids.includes(unknownId)) { + if (unknown !== undefined) + addEvidence(context, [ + ...unknown.supporting_evidence_ids, + ...unknown.contradicting_evidence_ids, + ...unknown.mutation_evidence_ids, + ]); addReason( context, "active-unknown", unknownId, "Active residual unknown is not permitted by the completion boundary.", ); + } + } for (const contradiction of context.coverage.contradictions) if ( contradiction.status === "active" && diff --git a/tests/boundary/mcp/reconstructionCoverageMcp.test.ts b/tests/boundary/mcp/reconstructionCoverageMcp.test.ts index d1478f5101..088b93d40b 100644 --- a/tests/boundary/mcp/reconstructionCoverageMcp.test.ts +++ b/tests/boundary/mcp/reconstructionCoverageMcp.test.ts @@ -9,11 +9,107 @@ import { reconstructionCoverageDataFixture, } from "../../../src/domain/reconstructionCoverage.fixture.js"; import { createEvidence } from "../../../src/domain/evidence.js"; +import { + createResidualUnknown, + recordUnknownInputSchema, + updateResidualUnknown, +} from "../../../src/domain/residualUnknown.js"; import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createServer } from "../../../src/server/createServer.js"; import { observed } from "../../fixtures/analysisExecution.js"; +it("uses the latest listed unknown revision when evaluating a boundary", async () => { + await withCoverageMcp(async ({ client }) => { + const base = currentFixtureCoverage(); + const [first, second] = base.evidence_bundle.records; + if (first === undefined || second === undefined) + throw new Error("Missing fixture Evidence"); + const unknownSource = createEvidence( + undefined, + { id: "unknown-source", name: "Unknown source", version: "1" }, + { + operation: "observe_unknown", + parameters: {}, + result: { evidence: true }, + }, + ); + const input = recordUnknownInputSchema.parse({ + question: "Is the route covered?", + supporting_evidence_ids: [unknownSource.evidence_id], + domain: "fixture", + severity: "medium", + required_authority: null, + required_confidence: "derived", + required_environment: null, + recommended_probes: [], + relationships: [], + }); + const open = createResidualUnknown(input, first.evidence_id, null); + const resolved = updateResidualUnknown( + open, + { + ...input, + unknown_id: open.unknown_id, + expected_revision: 1, + status: "resolved", + resolution: { + disposition: "withdrawn", + rationale: "No longer applicable", + evidence_ids: [], + }, + }, + second.evidence_id, + ); + const reopened = updateResidualUnknown( + resolved, + { + ...input, + unknown_id: open.unknown_id, + expected_revision: 2, + status: "investigating", + resolution: null, + }, + second.evidence_id, + ); + const coverage = (revisions: (typeof open)[]) => ({ + ...base, + evidence_bundle: createEvidenceBundle( + [...base.evidence_bundle.records, unknownSource], + revisions, + ), + residual_unknown_ids: [open.unknown_id, open.unknown_id], + }); + expect( + (await evaluate(client, coverage([open]))).structuredContent, + ).toMatchObject({ + status: "unknown", + evidence_ids: expect.arrayContaining([unknownSource.evidence_id]), + reasons: [ + expect.objectContaining({ + code: "active-unknown", + subject_id: open.unknown_id, + }), + ], + }); + expect( + (await evaluate(client, coverage([open, resolved]))).structuredContent, + ).toMatchObject({ status: "ready", reasons: [] }); + expect( + (await evaluate(client, coverage([open, resolved, reopened]))) + .structuredContent, + ).toMatchObject({ + status: "unknown", + reasons: [ + expect.objectContaining({ + code: "active-unknown", + subject_id: open.unknown_id, + }), + ], + }); + }); +}); + describe("reconstruction coverage inline Evidence", () => { it("evaluates inline fail-closed coverage without retaining request Evidence", async () => { await withCoverageMcp(async ({ client, session }) => {