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:
1 parent
592c46b001
commit
f8b65e7872
11 files changed
+724
-108
No files matched your search
@@ -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
@@ -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 };
|
||||
});
|
||||
Reference in new issue
Block a user