fix(reconstruction)!: bind proof to owner and obligation revision

Parse a named proof contract once and qualify exact obligation, owner, revision, fixture/case, and verifier/claim tuples. Preserve unavailable proof separately from applicable failed outcomes.

BREAKING CHANGE: ID-array reconstruction proof records no longer qualify for verified or ready ledgers. Producers must supply implementation and revision bindings with individual claim outcomes; ledger diagnostics now distinguish inapplicable, unavailable, and failed proof.
This commit is contained in:
morluto committed 2026-10-24 01:47:30 +08:00
1 parent 592c46b001
commit f8b65e7872
11 files changed
+724 -108

No files matched your search

+62 -16
View File
@@ -17,10 +17,9 @@ input Evidence bundle. Contradictions, duplicate definitions or owners,
residual unknowns, missing dependencies, and unavailable authority keep closure
open or failed.
Current proof qualification does not bind a passing record to the owner's
implementation digest or obligation revision. Read the
[proof applicability limitation](#proof-applicability) before relying on
`verified` or `ready` after either changes.
Every fixture and verifier proof must bind its outcome to the exact owner
implementation digest and obligation revision being evaluated. See
[proof applicability](#proof-applicability) for the caller-produced contract.
## Complete CLI request
@@ -343,16 +342,63 @@ Keep three checks separate:
- **Applicability:** the proof describes the implementation and obligation
revision currently being evaluated.
Current qualification checks authority, passing status and enumerated IDs, but
does not compare proof identity with `owner_sha256` or `obligation_version`.
Changing those fields while retaining old proof can therefore leave the result
`verified` and `ready` ([#6854](https://github.com/morluto/rea/issues/6854)).
Until that binding is enforced, clear the manifest binding's `verifier` after
an implementation or requirement revision changes and replace stale fixture
and verifier Evidence with proof for the new inputs. With `verifier: null`, the
required obligation cannot be verified and carries `missing-verifier`; an
authenticated old record alone does not establish applicability to the changed
implementation.
Proof producers supply observed Evidence with predicate type
`rea.reconstruction-proof`, operation `verify_reconstruction_obligations`, and
the following `normalized_result` shape, defined by
`reconstructionObligationProofSchema`. REA evaluates retained results; this
operation label does not expose a proof executor or run the manifest command.
```json
{
"bindings": [
{
"obligation_id": "protocol.rpc.cancel",
"obligation_version": 1,
"owner_sha256": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"fixtures": [
{
"fixture_id": "fixture.cancel.positive",
"case_kind": "positive",
"passed": true
}
],
"verifiers": [
{
"verifier_id": "verifier.cancel",
"claim_id": "claim.cancel",
"passed": true
}
]
}
]
}
```
`owner_sha256` is the same lowercase 64-hex SHA-256 commitment supplied by the
implementation owner in the manifest. The proof producer computes it from the
same documented inputs and canonicalization described above; REA compares the
commitments and does not recompute them. The proof's `obligation_version` must
exactly equal the evaluated obligation version, including for generated
obligations. Increase that version when revising a reviewed requirement.
Within one matching obligation/owner/revision binding, fixture proof must name
the exact `(fixture_id, case_kind)` pair, and verifier proof the exact
`(verifier_id, claim_id)` pair. Every selected claim must report `passed: true`;
a matching failed claim cannot be hidden by a duplicate passing claim.
Claims may share a record, but identities or outcomes from different bindings
are never joined. Fixture and verifier Evidence authority must also satisfy the
existing authority requirements.
Changing the owner digest or obligation version makes retained proof
inapplicable. Missing identity, including legacy ID-array proof records, keeps
the ledger open with `inapplicable-fixture-proof` or
`inapplicable-verifier-proof`; details distinguish malformed or missing metadata
(`invalid-proof`), `owner-mismatch`, `revision-mismatch`, and `claim-mismatch`.
An applicable false outcome produces `fixture-failed` or `verifier-failed` and
a failed required ledger. An unknown, unsupported, or skipped verifier declaration
produces `verifier-unavailable` and keeps closure open. Replace fixture and
verifier Evidence after implementation or requirement changes. Old Evidence
remains valid provenance and cannot establish current applicability.
The `summary` counts obligations. `by_status`, `by_application_layer`, and
`by_source_state` place each obligation in exactly one bucket.
@@ -364,8 +410,8 @@ obligation's `source_state` do not affect its authority bucket.
The tool returns every obligation inline. Its `ledger_id` and `closure_digest`
identify the complete result, including summaries, reports, ownership and
dependency graphs, Evidence links, and limitations. A new `closure_digest`
identifies a changed ledger; it does not establish that a verifier ran again or
that retained proof applies to the new owner or obligation revision.
identifies a changed ledger; it does not establish that a verifier ran again.
Applicability is checked against the retained proof's explicit bindings.
For named boundaries with explicit verifier contracts and package-proof kinds,
see [reconstruction coverage](reconstruction-coverage.md).
@@ -1,16 +1,49 @@
import { err, ok, type Result } from "../domain/result.js";
import {
reconstructionObligationProofSchema,
type ReconstructionObligationProof,
} from "../domain/reconstructionObligationProof.js";
import type { Evidence } from "../domain/evidence.js";
import type { ReconstructionObligation } from "../domain/reconstructionObligationLedgerSchemas.js";
type ProofAuthority = ReconstructionObligation["required_fixture_authority"];
type OriginalAuthority =
ReconstructionObligation["required_original_authority"];
interface ExpectedProof {
type ExpectedProof = {
readonly obligationId: string;
readonly fixtureId?: string;
readonly caseKind?: string;
readonly verifierId?: string;
readonly claimId?: string;
}
readonly obligationVersion: number;
readonly ownerSha256: string;
} & (
| {
readonly kind: "fixture";
readonly fixtureId: string;
readonly caseKind: ReconstructionObligation["required_case_kinds"][number];
}
| {
readonly kind: "verifier";
readonly verifierId: string;
readonly claimId: string;
}
);
type ProofRejection =
| "unobserved-proof"
| "unrecognized-proof"
| "invalid-proof"
| "insufficient-authority"
| "obligation-mismatch"
| "owner-mismatch"
| "revision-mismatch"
| "claim-mismatch"
| "failed-proof";
type ParsedProof = Result<
{
readonly authority: Evidence["authority"];
readonly result: ReconstructionObligationProof;
},
ProofRejection
>;
const proofEvidenceAuthorities: Readonly<
Record<ProofAuthority, readonly Evidence["authority"][]>
@@ -60,33 +93,64 @@ export const proofAuthoritySatisfies = (
return accepted[required].includes(actual);
};
/** Check that Evidence can authenticate the declared reconstruction authority. */
export const evidenceAuthoritySupportsProof = (
/** Parse retained proof once; authenticity is checked by bundle admission. */
export const parseReconstructionProofEvidence = (
evidence: Evidence,
authority: ProofAuthority,
expected: ExpectedProof,
): boolean => {
): ParsedProof => {
if (evidence.confidence !== "observed") return err("unobserved-proof");
if (
evidence.confidence !== "observed" ||
!proofEvidenceAuthorities[authority].includes(evidence.authority) ||
evidence.predicate_type !== "rea.reconstruction-proof" ||
evidence.operation !== "verify_reconstruction_obligations"
)
return false;
const result = evidence.normalized_result;
if (typeof result !== "object" || result === null || Array.isArray(result))
return false;
const includes = (key: string, value: string | undefined): boolean =>
value === undefined ||
(Array.isArray(result[key]) && result[key].includes(value));
return (
result.passed === true &&
includes("obligation_ids", expected.obligationId) &&
includes("fixture_ids", expected.fixtureId) &&
includes("case_kinds", expected.caseKind) &&
includes("verifier_ids", expected.verifierId) &&
includes("claim_ids", expected.claimId)
return err("unrecognized-proof");
const parsed = reconstructionObligationProofSchema.safeParse(
evidence.normalized_result,
);
return parsed.success
? ok({ authority: evidence.authority, result: parsed.data })
: err("invalid-proof");
};
/** Qualify one exact claim, without joining unrelated identities or results. */
export const qualifyReconstructionProof = (
proof: ParsedProof,
authority: ProofAuthority,
expected: ExpectedProof,
): Result<null, ProofRejection> => {
if (!proof.ok) return proof;
if (!proofEvidenceAuthorities[authority].includes(proof.value.authority))
return err("insufficient-authority");
const obligations = proof.value.result.bindings.filter(
(binding) => binding.obligation_id === expected.obligationId,
);
if (obligations.length === 0) return err("obligation-mismatch");
const owners = obligations.filter(
(binding) => binding.owner_sha256 === expected.ownerSha256,
);
if (owners.length === 0) return err("owner-mismatch");
const revisions = owners.filter(
(binding) => binding.obligation_version === expected.obligationVersion,
);
if (revisions.length === 0) return err("revision-mismatch");
const outcomes = revisions.flatMap((binding) =>
expected.kind === "fixture"
? binding.fixtures
.filter(
(fixture) =>
fixture.fixture_id === expected.fixtureId &&
fixture.case_kind === expected.caseKind,
)
.map(({ passed }) => passed)
: binding.verifiers
.filter(
(verifier) =>
verifier.verifier_id === expected.verifierId &&
verifier.claim_id === expected.claimId,
)
.map(({ passed }) => passed),
);
if (outcomes.length === 0) return err("claim-mismatch");
return outcomes.every((passed) => passed) ? ok(null) : err("failed-proof");
};
/** Check that Evidence observes the original at the required boundary. */
@@ -61,8 +61,8 @@ describe("reconstruction obligation ledger", () => {
expect(result.obligations[0]?.diagnostics.map(({ code }) => code)).toEqual(
expect.arrayContaining([
"weak-fixture-authority",
"weak-verifier-authority",
"inapplicable-fixture-proof",
"inapplicable-verifier-proof",
]),
);
});
@@ -72,11 +72,25 @@ describe("reconstruction obligation ledger", () => {
const evidence = boundProofEvidence(
"fixture-chain",
{
obligation_ids: ids,
fixture_ids: ids.map((id) => `fixture.${id}`),
case_kinds: ["positive"],
verifier_ids: ids.map((id) => `verifier.${id}`),
claim_ids: ids.map((id) => `claim.${id}`),
bindings: ids.map((id) => ({
obligation_id: id,
obligation_version: 1,
owner_sha256: "6".repeat(64),
fixtures: [
{
fixture_id: `fixture.${id}`,
case_kind: "positive" as const,
passed: true,
},
],
verifiers: [
{
verifier_id: `verifier.${id}`,
claim_id: `claim.${id}`,
passed: true,
},
],
})),
},
"shipped-artifact",
);
@@ -1,3 +1,7 @@
import {
reconstructionObligationProofSchema,
type ReconstructionObligationProof,
} from "../domain/reconstructionObligationProof.js";
import { PROCESS_PROVIDER } from "../domain/process/processEvidenceProvider.js";
import { buildReconstructionObligationLedgerEvidence } from "./ReconstructionObligationLedgerService.js";
import { resolveReconstructionObligationLedgerRequest } from "./ReconstructionObligationLedgerRequest.js";
@@ -56,13 +60,7 @@ export const processEvidence = (
/** Build verifier Evidence bound to enumerated fixture and obligation identities. */
export const boundProofEvidence = (
id: string,
result: {
readonly obligation_ids: readonly string[];
readonly fixture_ids: readonly string[];
readonly case_kinds: readonly string[];
readonly verifier_ids: readonly string[];
readonly claim_ids: readonly string[];
},
result: ReconstructionObligationProof,
authority: "shipped-artifact" | "controlled-replay" = "controlled-replay",
) =>
createEvidence(
@@ -72,14 +70,9 @@ export const boundProofEvidence = (
predicateType: "rea.reconstruction-proof",
operation: "verify_reconstruction_obligations",
parameters: {},
result: {
passed: true,
obligation_ids: [...result.obligation_ids],
fixture_ids: [...result.fixture_ids],
case_kinds: [...result.case_kinds],
verifier_ids: [...result.verifier_ids],
claim_ids: [...result.claim_ids],
},
result: jsonValueSchema.parse(
reconstructionObligationProofSchema.parse(result),
),
confidence: "observed",
authority,
},
@@ -130,11 +123,25 @@ export const readyProcessLedgerRequest =
(caseKind) => `packaged.${caseKind}`,
);
const proof = boundProofEvidence("packaged", {
obligation_ids: [obligation.obligation_id],
fixture_ids: fixtureIds,
case_kinds: obligation.required_case_kinds,
verifier_ids: ["verifier.packaged"],
claim_ids: ["claim.process"],
bindings: [
{
obligation_id: obligation.obligation_id,
obligation_version: obligation.obligation_version,
owner_sha256: "2".repeat(64),
fixtures: obligation.required_case_kinds.map((caseKind, index) => ({
fixture_id: fixtureIds[index]!,
case_kind: caseKind,
passed: true,
})),
verifiers: [
{
verifier_id: "verifier.packaged",
claim_id: "claim.process",
passed: true,
},
],
},
],
});
return request([capture, proof], {
manifest: {
@@ -284,3 +291,81 @@ export const deriveCandidates = (
manifest: { bindings: [], contradictions: [] },
}),
);
/** Exact reviewed proof control shared by public CLI/MCP applicability tests. */
export const readyReviewedLedgerRequest =
(): ReconstructionObligationLedgerInput => {
const original = authorityEvidence("reviewed-original", "shipped-artifact");
const obligation = reviewedObligation(
"review.identity",
original.evidence_id,
);
const proof = boundProofEvidence("reviewed", {
bindings: [
{
obligation_id: obligation.obligation_id,
obligation_version: obligation.obligation_version,
owner_sha256: "1".repeat(64),
fixtures: [
{
fixture_id: "fixture.positive",
case_kind: "positive",
passed: true,
},
],
verifiers: [
{
verifier_id: "verifier.reviewed",
claim_id: "claim.reviewed",
passed: true,
},
],
},
],
});
return request([original, proof], {
reviewed_obligations: [obligation],
manifest: {
bindings: [
{
obligation_id: obligation.obligation_id,
owner: {
module_path: "src/owner.ts",
symbol: "run",
owner_sha256: "1".repeat(64),
},
parser_type: null,
original_cases: [
{
kind: "positive",
evidence_id: original.evidence_id,
location: "/normalized_result",
},
],
fixtures: [
{
fixture_id: "fixture.positive",
case_kind: "positive",
authority: "unit",
evidence_ids: [proof.evidence_id],
},
],
verifier: {
verifier_id: "verifier.reviewed",
claim_id: "claim.reviewed",
command: "synthetic proof (not executed)",
authority: "unit",
status: "pass",
result_evidence_id: proof.evidence_id,
enumerated_obligation_ids: [obligation.obligation_id],
nondeterminism: {
mode: "total-order",
specification: "Single result",
},
},
},
],
contradictions: [],
},
});
};
@@ -19,7 +19,8 @@ import {
} from "../domain/reconstructionObligationLedgerIntegrity.js";
import type { ReconstructionObligationCandidate } from "./ReconstructionObligationCandidates.js";
import {
evidenceAuthoritySupportsProof,
parseReconstructionProofEvidence,
qualifyReconstructionProof,
evidenceSupportsOriginalAuthority,
proofAuthoritySatisfies,
} from "./ReconstructionObligationAuthority.js";
@@ -43,6 +44,10 @@ type Binding = NonNullable<ReconstructionObligation["binding"]>;
interface EvaluationContext {
readonly candidates: ReadonlyMap<string, ReconstructionObligationCandidate>;
readonly evidence: ReadonlyMap<string, Evidence>;
readonly proofs: ReadonlyMap<
string,
ReturnType<typeof parseReconstructionProofEvidence>
>;
readonly bindings: ReadonlyMap<string, readonly Binding[]>;
readonly contradictions: ReturnType<typeof groupManifestContradictions>;
}
@@ -118,6 +123,12 @@ const createEvaluationContext = (
candidates.map((candidate) => [candidate.obligation_id, candidate]),
),
evidence,
proofs: new Map(
[...evidence].map(([id, record]) => [
id,
parseReconstructionProofEvidence(record),
]),
),
bindings: groupManifestBindings(manifest),
contradictions: groupManifestContradictions(manifest),
});
@@ -272,30 +283,51 @@ const evaluateBinding = (
);
continue;
}
const qualifications = fixtures.map((fixture) => ({
authoritySatisfied: proofAuthoritySatisfies(
fixture.authority,
candidate.required_fixture_authority,
),
proofs: fixture.evidence_ids.map((id) => {
const proof = context.proofs.get(id);
return proof === undefined
? ({ ok: false, error: "missing-evidence" } as const)
: qualifyReconstructionProof(proof, fixture.authority, {
kind: "fixture",
obligationId: candidate.obligation_id,
obligationVersion: candidate.obligation_version,
ownerSha256: binding.owner.owner_sha256,
fixtureId: fixture.fixture_id,
caseKind: fixture.case_kind,
});
}),
}));
if (
!fixtures.some(
(fixture) =>
proofAuthoritySatisfies(
fixture.authority,
candidate.required_fixture_authority,
) &&
fixture.evidence_ids.every((id) => {
const evidence = context.evidence.get(id);
return (
evidence !== undefined &&
evidenceAuthoritySupportsProof(evidence, fixture.authority, {
obligationId: candidate.obligation_id,
fixtureId: fixture.fixture_id,
caseKind: fixture.case_kind,
})
);
}),
qualifications.some(
({ authoritySatisfied, proofs }) =>
authoritySatisfied && proofs.every((proof) => proof.ok),
)
)
continue;
const failures = qualifications.flatMap(({ proofs }) =>
proofs.flatMap((proof) => (proof.ok ? [] : [proof.error])),
);
if (
qualifications.some(({ authoritySatisfied }) => !authoritySatisfied) ||
failures.includes("insufficient-authority")
)
addDiagnostic(
diagnostics,
"weak-fixture-authority",
`${caseKind} fixture lacks ${candidate.required_fixture_authority} authority or authenticated Evidence.`,
`${caseKind} fixture lacks ${candidate.required_fixture_authority} authority.`,
);
if (failures.length > 0)
addDiagnostic(
diagnostics,
failures.includes("failed-proof")
? "fixture-failed"
: "inapplicable-fixture-proof",
`${caseKind} fixture proof qualification: ${[...new Set(failures)].sort().join(", ")}.`,
);
}
evaluateVerifier(candidate, binding, context, diagnostics);
@@ -333,23 +365,42 @@ const evaluateVerifier = (
!proofAuthoritySatisfies(
verifier.authority,
candidate.required_verifier_authority,
) ||
(resultEvidence !== undefined &&
!evidenceAuthoritySupportsProof(resultEvidence, verifier.authority, {
obligationId: candidate.obligation_id,
verifierId: verifier.verifier_id,
claimId: verifier.claim_id,
}))
)
)
addDiagnostic(
diagnostics,
"weak-verifier-authority",
`Verifier authority does not satisfy ${candidate.required_verifier_authority}.`,
);
const proof = context.proofs.get(verifier.result_evidence_id);
if (proof !== undefined) {
const qualification = qualifyReconstructionProof(
proof,
verifier.authority,
{
kind: "verifier",
obligationId: candidate.obligation_id,
obligationVersion: candidate.obligation_version,
ownerSha256: binding.owner.owner_sha256,
verifierId: verifier.verifier_id,
claimId: verifier.claim_id,
},
);
if (!qualification.ok)
addDiagnostic(
diagnostics,
qualification.error === "failed-proof"
? "verifier-failed"
: qualification.error === "insufficient-authority"
? "weak-verifier-authority"
: "inapplicable-verifier-proof",
`Verifier proof qualification: ${qualification.error}.`,
);
}
if (verifier.status !== "pass")
addDiagnostic(
diagnostics,
"verifier-failed",
verifier.status === "fail" ? "verifier-failed" : "verifier-unavailable",
`Latest verifier status is ${verifier.status}.`,
);
};
@@ -74,7 +74,8 @@ export const reconstructionObligationLedgerStatus = (
requiredOpen.some(
({ status, diagnostics }) =>
status === "contradicted" ||
hasReconstructionObligationDiagnostic(diagnostics, "verifier-failed"),
hasReconstructionObligationDiagnostic(diagnostics, "verifier-failed") ||
hasReconstructionObligationDiagnostic(diagnostics, "fixture-failed"),
)
)
return "failed";
@@ -210,11 +210,15 @@ const diagnosticSchema = z.strictObject({
"weak-original-case-authority",
"missing-case",
"weak-fixture-authority",
"inapplicable-fixture-proof",
"fixture-failed",
"missing-verifier",
"verifier-does-not-enumerate",
"missing-verifier-result",
"weak-verifier-authority",
"verifier-failed",
"verifier-unavailable",
"inapplicable-verifier-proof",
"static-only",
"contradiction",
"contradiction-evidence-missing",
@@ -0,0 +1,38 @@
import { z } from "zod";
import { digestSchema } from "./digests.js";
import { STABLE_IDENTIFIER_PATTERN } from "./stringPatterns.js";
import { obligationCaseKindSchema } from "./reconstructionObligationLedgerSchemas.js";
const stableIdSchema = z.string().regex(STABLE_IDENTIFIER_PATTERN);
/** Caller-produced proof binds each claim to one implementation and revision. */
export const reconstructionObligationProofSchema = z.strictObject({
bindings: z
.array(
z.strictObject({
obligation_id: stableIdSchema,
obligation_version: z.int().min(1),
owner_sha256: digestSchema,
fixtures: z.array(
z.strictObject({
fixture_id: stableIdSchema,
case_kind: obligationCaseKindSchema,
passed: z.boolean(),
}),
),
verifiers: z.array(
z.strictObject({
verifier_id: stableIdSchema,
claim_id: stableIdSchema,
passed: z.boolean(),
}),
),
}),
)
.min(1),
});
export type ReconstructionObligationProof = z.infer<
typeof reconstructionObligationProofSchema
>;
@@ -1,3 +1,4 @@
import { reconstructionProofApplicabilityCases } from "../../fixtures/reconstructionProofApplicability.js";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { writeFile } from "node:fs/promises";
@@ -308,3 +309,51 @@ cliTest(
).toBe(true);
},
);
describe("reconstruction proof applicability through CLI", () => {
for (const {
name,
input,
status,
reason,
codes,
} of reconstructionProofApplicabilityCases())
it(
name,
async () => {
const { stdout } = await execute(
process.execPath,
[
"scripts/rea.mjs",
"build-reconstruction-obligation-ledger",
JSON.stringify(input),
"--json",
],
{
cwd: process.cwd(),
env: process.env,
maxBuffer: 16 * 1_024 * 1_024,
},
);
const ledger = reconstructionObligationLedgerSchema.parse(
JSON.parse(stdout).normalized_result,
);
expect(ledger.status).toBe(status);
const obligation = ledger.obligations[0]!;
expect(obligation.status).toBe(
status === "ready" ? "verified" : "implemented",
);
expect(obligation.diagnostics.map(({ code }) => code).sort()).toEqual(
[...codes].sort(),
);
if (reason === null) expect(obligation.diagnostics).toEqual([]);
else
expect(
obligation.diagnostics.some(({ detail }) =>
detail.includes(reason),
),
).toBe(true);
},
20_000,
);
});
@@ -1,3 +1,6 @@
import { reconstructionProofApplicabilityCases } from "../../fixtures/reconstructionProofApplicability.js";
import { reconstructionObligationLedgerSchema } from "../../../src/domain/reconstructionObligationLedgerSchemas.js";
import type { ReconstructionObligationManifest } from "../../../src/domain/reconstructionObligationLedgerSchemas.js";
import { ok as resultOk } from "../../../src/domain/result.js";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { describe, expect, it } from "vitest";
@@ -5,7 +8,10 @@ import { describe, expect, it } from "vitest";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import {
createEvidence,
evidenceSchema,
} from "../../../src/domain/evidence.js";
import { PROCESS_PROVIDER } from "../../../src/domain/process/processEvidenceProvider.js";
import {
createResidualUnknown,
@@ -24,6 +30,7 @@ import { observed } from "../../fixtures/analysisExecution.js";
type LedgerRequest = (
evidenceBundle: unknown,
reviewedObligations?: readonly unknown[],
manifest?: ReconstructionObligationManifest,
) => ReturnType<Client["callTool"]>;
const withLedgerClient = async (
@@ -40,15 +47,20 @@ const withLedgerClient = async (
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
await run((evidenceBundle, reviewedObligations = []) =>
client.callTool({
name: "build_reconstruction_obligation_ledger",
arguments: {
evidence_bundle: evidenceBundle,
reviewed_obligations: reviewedObligations,
manifest: { bindings: [], contradictions: [] },
},
}),
await run(
(
evidenceBundle,
reviewedObligations = [],
manifest = { bindings: [], contradictions: [] },
) =>
client.callTool({
name: "build_reconstruction_obligation_ledger",
arguments: {
evidence_bundle: evidenceBundle,
reviewed_obligations: reviewedObligations,
manifest,
},
}),
);
} finally {
await client.close();
@@ -375,3 +387,41 @@ describe("reconstruction obligation ledger MCP parity", () => {
}
});
});
describe("reconstruction proof applicability through MCP", () => {
for (const {
name,
input,
status,
reason,
codes,
} of reconstructionProofApplicabilityCases())
it(name, async () => {
await withLedgerClient(async (request) => {
const response = await request(
input.evidence_bundle,
input.reviewed_obligations,
input.manifest,
);
expect(response.isError).not.toBe(true);
const ledger = reconstructionObligationLedgerSchema.parse(
evidenceSchema.parse(response.structuredContent).normalized_result,
);
expect(ledger.status).toBe(status);
const obligation = ledger.obligations[0]!;
expect(obligation.status).toBe(
status === "ready" ? "verified" : "implemented",
);
expect(obligation.diagnostics.map(({ code }) => code).sort()).toEqual(
[...codes].sort(),
);
if (reason === null) expect(obligation.diagnostics).toEqual([]);
else
expect(
obligation.diagnostics.some(({ detail }) =>
detail.includes(reason),
),
).toBe(true);
});
});
});
+214
View File
@@ -0,0 +1,214 @@
import { readyReviewedLedgerRequest } from "../../src/application/ReconstructionObligationLedger.fixture.js";
import { createEvidence } from "../../src/domain/evidence.js";
import { createEvidenceBundle } from "../../src/domain/evidenceBundle.js";
import { jsonValueSchema } from "../../src/domain/jsonValue.js";
import { reconstructionObligationProofSchema } from "../../src/domain/reconstructionObligationProof.js";
import type { ReconstructionObligationLedgerInput } from "../../src/domain/reconstructionObligationLedgerSchemas.js";
const replaceProof = (
input: ReconstructionObligationLedgerInput,
result: unknown,
): void => {
const original = input.evidence_bundle.records.find(
(record) => record.predicate_type === "rea.reconstruction-proof",
)!;
const replacement = createEvidence(undefined, original.provider, {
predicateType: original.predicate_type,
operation: original.operation,
parameters: original.parameters,
result: jsonValueSchema.parse(result),
confidence: original.confidence,
authority: original.authority,
});
input.evidence_bundle = createEvidenceBundle(
input.evidence_bundle.records.map((record) =>
record.evidence_id === original.evidence_id ? replacement : record,
),
);
const binding = input.manifest.bindings[0]!;
binding.fixtures[0]!.evidence_ids = [replacement.evidence_id];
binding.verifier!.result_evidence_id = replacement.evidence_id;
};
const applicabilitySpecifications = [
{
name: "matching implementation and revision",
codes: [],
status: "ready",
reason: null,
},
{
name: "stronger proof authority",
codes: [],
status: "ready",
reason: null,
},
{
name: "conflicting duplicate outcomes",
codes: ["verifier-failed"],
status: "failed",
reason: "failed-proof",
},
{
name: "failed proof for stale implementation",
codes: ["inapplicable-fixture-proof", "inapplicable-verifier-proof"],
status: "open",
reason: "owner-mismatch",
},
{
name: "changed implementation",
codes: ["inapplicable-fixture-proof", "inapplicable-verifier-proof"],
status: "open",
reason: "owner-mismatch",
},
{
name: "changed obligation revision",
codes: ["inapplicable-fixture-proof", "inapplicable-verifier-proof"],
status: "open",
reason: "revision-mismatch",
},
{
name: "legacy ID-only proof",
codes: ["inapplicable-fixture-proof", "inapplicable-verifier-proof"],
status: "open",
reason: "invalid-proof",
},
{
name: "missing implementation commitment",
codes: ["inapplicable-fixture-proof", "inapplicable-verifier-proof"],
status: "open",
reason: "invalid-proof",
},
{
name: "missing obligation revision",
codes: ["inapplicable-fixture-proof", "inapplicable-verifier-proof"],
status: "open",
reason: "invalid-proof",
},
{
name: "fixture case pair mismatch",
codes: ["inapplicable-fixture-proof"],
status: "open",
reason: "claim-mismatch",
},
{
name: "verifier claim pair mismatch",
codes: ["inapplicable-verifier-proof"],
status: "open",
reason: "claim-mismatch",
},
{
name: "claims in a stale binding",
codes: ["inapplicable-fixture-proof", "inapplicable-verifier-proof"],
status: "open",
reason: "claim-mismatch",
},
{
name: "failed fixture",
codes: ["fixture-failed"],
status: "failed",
reason: "failed-proof",
},
{
name: "failed verifier",
codes: ["verifier-failed"],
status: "failed",
reason: "failed-proof",
},
{
name: "unknown verifier declaration",
codes: ["verifier-unavailable"],
status: "open",
reason: "Latest verifier status is unknown",
},
] as const;
/** Authenticated synthetic Evidence; no declared verifier command executes. */
export const reconstructionProofApplicabilityCases = () =>
applicabilitySpecifications.map((specification) => {
const input = readyReviewedLedgerRequest();
const result = reconstructionObligationProofSchema.parse(
input.evidence_bundle.records.find(
(record) => record.predicate_type === "rea.reconstruction-proof",
)!.normalized_result,
);
const proof = result.bindings[0]!;
switch (specification.name) {
case "stronger proof authority":
input.manifest.bindings[0]!.fixtures[0]!.authority = "integration";
input.manifest.bindings[0]!.verifier!.authority = "integration";
break;
case "conflicting duplicate outcomes":
proof.verifiers.push({ ...proof.verifiers[0]!, passed: false });
replaceProof(input, result);
break;
case "failed proof for stale implementation":
input.manifest.bindings[0]!.owner.owner_sha256 = "2".repeat(64);
proof.fixtures[0]!.passed = false;
proof.verifiers[0]!.passed = false;
replaceProof(input, result);
break;
case "changed implementation":
input.manifest.bindings[0]!.owner.owner_sha256 = "2".repeat(64);
break;
case "changed obligation revision":
input.reviewed_obligations[0]!.obligation_version = 2;
break;
case "legacy ID-only proof":
replaceProof(input, {
passed: true,
obligation_ids: [proof.obligation_id],
fixture_ids: [proof.fixtures[0]!.fixture_id],
case_kinds: ["positive"],
verifier_ids: [proof.verifiers[0]!.verifier_id],
claim_ids: [proof.verifiers[0]!.claim_id],
});
break;
case "missing implementation commitment":
Reflect.deleteProperty(proof, "owner_sha256");
replaceProof(input, result);
break;
case "missing obligation revision":
Reflect.deleteProperty(proof, "obligation_version");
replaceProof(input, result);
break;
case "fixture case pair mismatch":
proof.fixtures[0]!.case_kind = "negative";
proof.fixtures.push({
fixture_id: "fixture.other",
case_kind: "positive",
passed: true,
});
replaceProof(input, result);
break;
case "verifier claim pair mismatch":
proof.verifiers[0]!.claim_id = "claim.other";
proof.verifiers.push({
verifier_id: "verifier.other",
claim_id: "claim.reviewed",
passed: true,
});
replaceProof(input, result);
break;
case "claims in a stale binding":
result.bindings.push({
...structuredClone(proof),
owner_sha256: "2".repeat(64),
});
proof.fixtures = [];
proof.verifiers = [];
replaceProof(input, result);
break;
case "failed fixture":
proof.fixtures[0]!.passed = false;
replaceProof(input, result);
break;
case "failed verifier":
proof.verifiers[0]!.passed = false;
replaceProof(input, result);
break;
case "unknown verifier declaration":
input.manifest.bindings[0]!.verifier!.status = "unknown";
break;
}
return { ...specification, input };
});