diff --git a/src/cli/applicationCommands.ts b/src/cli/applicationCommands.ts index 4cc918b767..a52bf7af06 100644 --- a/src/cli/applicationCommands.ts +++ b/src/cli/applicationCommands.ts @@ -1,4 +1,3 @@ -import { reconstructionCoverageEvaluationInputSchema } from "../domain/reconstructionCoverageInput.js"; import { Cli, z } from "incur"; import { CLI_COMMANDS } from "../cliCommandNames.js"; @@ -145,7 +144,7 @@ const registerCoverageCommand = (cli: CliInstance, logger: Logger): void => logger, name: CLI_COMMANDS.evaluateReconstructionCoverage, description: "Evaluate inline fail-closed reconstruction coverage", - inputSchema: reconstructionCoverageEvaluationInputSchema, + inputSchema: z.unknown(), workflow: async (input) => { const { evaluateReconstructionCoverage } = await import("../application/ReconstructionCoverageService.js"); diff --git a/src/domain/reconstructionCoverage.fixture.ts b/src/domain/reconstructionCoverage.fixture.ts index 78d4b892be..b3052af438 100644 --- a/src/domain/reconstructionCoverage.fixture.ts +++ b/src/domain/reconstructionCoverage.fixture.ts @@ -1,8 +1,12 @@ +import { expect } from "vitest"; + import { createEvidence } from "./evidence.js"; import { createEvidenceBundle } from "./evidenceBundle.js"; import { createReconstructionCoverageData, createReconstructionVerifierContract, + evaluateParsedReconstructionClosure, + readReconstructionCoverageData, } from "./reconstructionCoverage.js"; const digest = (character: string): string => character.repeat(64); @@ -149,6 +153,31 @@ export const completeReconstructionCoverageData = () => { }); }; +/** Assert successful domain parsing and evaluation for semantic test fixtures. */ +export const assertReconstructionCoverageEvaluation = ( + input: unknown, + boundaryId: string, + nowEpochMs: number, +) => { + const parsed = readReconstructionCoverageData(input, ["coverage"]); + expect(parsed.ok).toBe(true); + if (!parsed.ok) + throw new Error("Expected valid reconstruction coverage fixture", { + cause: parsed.error, + }); + const result = evaluateParsedReconstructionClosure( + parsed.value, + boundaryId, + nowEpochMs, + ); + expect(result.ok).toBe(true); + if (!result.ok) + throw new Error("Expected reconstruction coverage evaluation success", { + cause: result.error, + }); + return result.value; +}; + /** Complete workspace with its first verifier result and boundary narrowed once. */ export const completeReconstructionCoverageFixture = () => { const workspace = completeReconstructionCoverageData(); diff --git a/src/domain/reconstructionCoverage.test.ts b/src/domain/reconstructionCoverage.test.ts index 7663b10a59..e1a84e46c3 100644 --- a/src/domain/reconstructionCoverage.test.ts +++ b/src/domain/reconstructionCoverage.test.ts @@ -3,13 +3,14 @@ import { describe, expect, it } from "vitest"; import { completeReconstructionCoverageData, completeReconstructionCoverageFixture, + assertReconstructionCoverageEvaluation, RECONSTRUCTION_COVERAGE_NOW, reconstructionCoverageEvidenceId, } from "./reconstructionCoverage.fixture.js"; import { createReconstructionCoverageData, createReconstructionVerifierContract, - evaluateReconstructionClosure, + evaluateParsedReconstructionClosure, readReconstructionCoverageData, type ReconstructionCoverageData, } from "./reconstructionCoverage.js"; @@ -20,7 +21,7 @@ it("preserves duplicate coverage owners instead of overwriting them in shared in const workspace = completeReconstructionCoverageData(); const owner = workspace.owners[0]; if (owner === undefined) throw new Error("Expected fixture owner"); - const result = evaluateReconstructionClosure( + const result = assertReconstructionCoverageEvaluation( { ...workspace, owners: [owner, owner], @@ -44,7 +45,7 @@ describe("reconstruction coverage closure", () => { const workspace = completeReconstructionCoverageData(); expect( - evaluateReconstructionClosure( + assertReconstructionCoverageEvaluation( workspace, "replacement.cli", RECONSTRUCTION_COVERAGE_NOW, @@ -78,7 +79,7 @@ describe("reconstruction coverage closure", () => { }); expect( - evaluateReconstructionClosure( + assertReconstructionCoverageEvaluation( incomplete, "replacement.cli", RECONSTRUCTION_COVERAGE_NOW, @@ -115,7 +116,7 @@ describe("reconstruction coverage closure", () => { boundaries: [{ ...boundary, required_surface_ids: missingSurfaces }], }); - const result = evaluateReconstructionClosure( + const result = assertReconstructionCoverageEvaluation( incomplete, boundary.boundary_id, RECONSTRUCTION_COVERAGE_NOW, @@ -147,7 +148,7 @@ describe("reconstruction coverage closure", () => { ], }); - const result = evaluateReconstructionClosure( + const result = assertReconstructionCoverageEvaluation( changed, "replacement.cli", RECONSTRUCTION_COVERAGE_NOW, @@ -173,7 +174,7 @@ describe("reconstruction coverage closure", () => { }); expect( - evaluateReconstructionClosure( + assertReconstructionCoverageEvaluation( stale, "replacement.cli", RECONSTRUCTION_COVERAGE_NOW, @@ -233,7 +234,7 @@ describe("reconstruction coverage verifier observations", () => { }); expect( - evaluateReconstructionClosure( + assertReconstructionCoverageEvaluation( shared, boundary.boundary_id, RECONSTRUCTION_COVERAGE_NOW, @@ -281,7 +282,7 @@ describe("reconstruction coverage verifier selection", () => { ], }); expect( - evaluateReconstructionClosure( + assertReconstructionCoverageEvaluation( ambiguous, boundary.boundary_id, RECONSTRUCTION_COVERAGE_NOW, @@ -298,7 +299,7 @@ describe("reconstruction coverage verifier selection", () => { verifier_results: [], }); expect( - evaluateReconstructionClosure( + assertReconstructionCoverageEvaluation( missing, boundary.boundary_id, RECONSTRUCTION_COVERAGE_NOW, @@ -330,12 +331,12 @@ describe("reconstruction coverage conflicting latest verifier observations", () verifier_results: [failed, result], }); - const firstPassResult = evaluateReconstructionClosure( + const firstPassResult = assertReconstructionCoverageEvaluation( firstPass, boundary.boundary_id, RECONSTRUCTION_COVERAGE_NOW, ); - const firstFailResult = evaluateReconstructionClosure( + const firstFailResult = assertReconstructionCoverageEvaluation( firstFail, boundary.boundary_id, RECONSTRUCTION_COVERAGE_NOW, @@ -384,12 +385,12 @@ describe("reconstruction coverage equivalent latest verifier observations", () = ...workspace, verifier_results: [equivalent, first], }); - const forwardResult = evaluateReconstructionClosure( + const forwardResult = assertReconstructionCoverageEvaluation( forward, boundary.boundary_id, RECONSTRUCTION_COVERAGE_NOW, ); - const reversedResult = evaluateReconstructionClosure( + const reversedResult = assertReconstructionCoverageEvaluation( reversed, boundary.boundary_id, RECONSTRUCTION_COVERAGE_NOW, @@ -421,7 +422,7 @@ describe("reconstruction coverage verifier timestamp selection", () => { }); expect( - evaluateReconstructionClosure( + assertReconstructionCoverageEvaluation( changed, boundary.boundary_id, RECONSTRUCTION_COVERAGE_NOW, @@ -441,7 +442,7 @@ describe("reconstruction coverage verifier timestamp selection", () => { }); expect( - evaluateReconstructionClosure( + assertReconstructionCoverageEvaluation( changed, "replacement.cli", RECONSTRUCTION_COVERAGE_NOW, @@ -478,7 +479,7 @@ describe("reconstruction coverage verifier future timestamps", () => { }); expect( - evaluateReconstructionClosure( + assertReconstructionCoverageEvaluation( changed, "replacement.cli", Date.parse("2026-07-16T12:00:00.123Z") + 0.5, @@ -491,8 +492,15 @@ describe("reconstruction coverage verifier future timestamps", () => { it("rejects a non-finite evaluation clock", () => { const workspace = completeReconstructionCoverageData(); + const parsed = readReconstructionCoverageData(workspace, ["coverage"]); + expect(parsed.ok).toBe(true); + if (!parsed.ok) throw parsed.error; expect(() => - evaluateReconstructionClosure(workspace, "replacement.cli", Number.NaN), + evaluateParsedReconstructionClosure( + parsed.value, + "replacement.cli", + Number.NaN, + ), ).toThrow("Expected finite epoch milliseconds."); }); @@ -507,7 +515,7 @@ describe("reconstruction coverage verifier future timestamps", () => { }); expect( - evaluateReconstructionClosure( + assertReconstructionCoverageEvaluation( changed, "replacement.cli", RECONSTRUCTION_COVERAGE_NOW, @@ -533,7 +541,7 @@ describe("reconstruction coverage verifier future timestamps", () => { })), }); - const result = evaluateReconstructionClosure( + const result = assertReconstructionCoverageEvaluation( incomplete, "replacement.cli", RECONSTRUCTION_COVERAGE_NOW, diff --git a/src/domain/reconstructionCoverage.ts b/src/domain/reconstructionCoverage.ts index c8ce64e9ac..5fd1c73e85 100644 --- a/src/domain/reconstructionCoverage.ts +++ b/src/domain/reconstructionCoverage.ts @@ -325,23 +325,6 @@ export const createReconstructionVerifierContract = ( }); }; -/** Evaluate inline coverage data against one named fail-closed boundary. */ -export const evaluateReconstructionClosure = ( - coverageInput: unknown, - boundaryId: string, - nowEpochMs: number, -): ReconstructionClosureResult => { - const parsed = readReconstructionCoverageData(coverageInput, ["coverage"]); - if (!parsed.ok) throw parsed.error; - const result = evaluateParsedReconstructionClosure( - parsed.value, - boundaryId, - nowEpochMs, - ); - if (!result.ok) throw result.error; - return result.value; -}; - /** Evaluate the owner's authenticated workspace and lookup facts. */ export const evaluateParsedReconstructionClosure = ( parsed: ParsedReconstructionCoverage, diff --git a/src/domain/reconstructionCoverageEvidence.test.ts b/src/domain/reconstructionCoverageEvidence.test.ts index aea275a3d9..6b0212ecb0 100644 --- a/src/domain/reconstructionCoverageEvidence.test.ts +++ b/src/domain/reconstructionCoverageEvidence.test.ts @@ -4,12 +4,12 @@ import { createEvidence, type Evidence } from "./evidence.js"; import { createEvidenceBundle } from "./evidenceBundle.js"; import { completeReconstructionCoverageFixture, + assertReconstructionCoverageEvaluation, RECONSTRUCTION_COVERAGE_NOW, } from "./reconstructionCoverage.fixture.js"; import { createReconstructionCoverageData, createReconstructionVerifierContract, - evaluateReconstructionClosure, type ReconstructionCoverageData, } from "./reconstructionCoverage.js"; @@ -79,7 +79,7 @@ const withPackageProof = ( }; const evaluate = (coverage: ReconstructionCoverageData) => - evaluateReconstructionClosure( + assertReconstructionCoverageEvaluation( coverage, "replacement.cli", RECONSTRUCTION_COVERAGE_NOW, diff --git a/src/server/registerApplicationTools/coverage.ts b/src/server/registerApplicationTools/coverage.ts index 3b986da1ba..8957d170f1 100644 --- a/src/server/registerApplicationTools/coverage.ts +++ b/src/server/registerApplicationTools/coverage.ts @@ -4,7 +4,7 @@ import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { evaluateReconstructionCoverage } from "../../application/ReconstructionCoverageService.js"; import { applicationToolContract } from "../../contracts/applicationToolContracts.js"; import { logToolExecution } from "../toolLogging.js"; -import { toolRegistrationOptions } from "../toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "../toolRegistrationOptions.js"; import type { ApplicationToolRegistration } from "./types.js"; const contract = applicationToolContract("evaluate_reconstruction_coverage"); @@ -16,7 +16,7 @@ export const registerCoverageTools = ( ): void => { server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), async (input, context) => runAdmittedToolOperation( server, diff --git a/tests/boundary/cli/reconstructionCoverageCli.test.ts b/tests/boundary/cli/reconstructionCoverageCli.test.ts index 68d15e1fcf..618625fb74 100644 --- a/tests/boundary/cli/reconstructionCoverageCli.test.ts +++ b/tests/boundary/cli/reconstructionCoverageCli.test.ts @@ -3,6 +3,7 @@ import { join } from "node:path"; import { expect } from "vitest"; import { completeReconstructionCoverageData } from "../../../src/domain/reconstructionCoverage.fixture.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest, type TestCli } from "../../support/cli/cliFixture.js"; @@ -65,3 +66,52 @@ cliTest( }); }, ); + +cliTest( + "preserves shared integrity diagnostics for malformed source Evidence", + async ({ cli }) => { + const coverage = currentCoverage(); + const malformed = { + ...coverage, + evidence_bundle: { + ...coverage.evidence_bundle, + records: coverage.evidence_bundle.records.map((record, index) => + index === 0 + ? { ...record, provider: { ...record.provider, name: 3 } } + : record, + ), + }, + }; + const call = await cli.sharedMcpCall?.(); + if (call === undefined) throw new Error("Expected file MCP session"); + const mcp = await call("evaluate_reconstruction_coverage", { + coverage: malformed, + boundary_id: "replacement.cli", + }); + expect(mcp.isError).toBe(true); + expect(parseMcpToolError(mcp)).toMatchObject({ + error: { code: "evidence_integrity_mismatch" }, + }); + const result = await evaluate(cli, malformed); + expect(result.exitCode).toBe(1); + expect(result.json).toMatchObject({ + code: "evidence_integrity_mismatch", + category: "integrity_mismatch", + }); + }, +); + +cliTest( + "locates malformed coverage through its shared workflow owner", + async ({ cli }) => { + const result = await evaluate(cli, { ...currentCoverage(), owners: 3 }); + expect(result.exitCode).toBe(1); + expect(result.json).toMatchObject({ + code: "invalid_request", + details: { + operation: "evaluate_reconstruction_coverage", + issues: [{ path: ["coverage", "owners"], reason: "invalid_type" }], + }, + }); + }, +); diff --git a/tests/boundary/mcp/reconstructionCoverageMcp.test.ts b/tests/boundary/mcp/reconstructionCoverageMcp.test.ts index 525049c1c5..5919c250a2 100644 --- a/tests/boundary/mcp/reconstructionCoverageMcp.test.ts +++ b/tests/boundary/mcp/reconstructionCoverageMcp.test.ts @@ -123,6 +123,42 @@ describe("reconstruction coverage inline Evidence", () => { }); describe("reconstruction coverage selection", () => { + it.each([ + { + label: "unknown root argument", + selection: { boundary_id: "replacement.cli", unexpected: true }, + path: ["unexpected"], + reason: "unknown_argument", + }, + { + label: "missing boundary", + selection: {}, + path: ["boundary_id"], + reason: "missing_argument", + }, + ])( + "rejects $label through the shared owner", + async ({ selection, path, reason }) => { + await withCoverageMcp(async ({ client, session }) => { + const result = await client.callTool({ + name: "evaluate_reconstruction_coverage", + arguments: { coverage: currentFixtureCoverage(), ...selection }, + }); + expect(result.isError).toBe(true); + expect(parseMcpToolError(result)).toMatchObject({ + error: { + code: "invalid_request", + details: { + operation: "evaluate_reconstruction_coverage", + issues: [{ path, reason }], + }, + }, + }); + expect(session.exportEvidenceBundle().records).toEqual([]); + }); + }, + ); + it("reports ambiguity when multiple verifier results cover one claim", async () => { await withCoverageMcp(async ({ client }) => { const coverage = currentFixtureCoverage();