fix(reconstruction): validate all retained case Evidence references

Preserve format and Evidence semantics at their owning boundaries, with focused regression coverage and documentation where applicable.

Fixes #6770
This commit is contained in:
morluto
2026-10-23 23:36:21 +08:00
parent e2efd10b70
commit 50af8de5e8
4 changed files with 102 additions and 24 deletions
+6 -4
View File
@@ -315,10 +315,12 @@ A manifest binding names one owner and its exact implementation digest. Both
}
```
All referenced Evidence IDs must exist in the bundle. A green unit test cannot
close a `packaged-process` obligation. A static-only source cannot close a
runtime or process claim. Marking a claim `blocked` or `out-of-scope` records the
disposition but does not silently count it as verified.
A ready ledger requires every retained Evidence reference to resolve in the
bundle, including references on optional case kinds. Unresolved references stay
visible in the ledger and produce diagnostics; they cannot close the obligation.
A green unit test cannot close a `packaged-process` obligation. A static-only
source cannot close a runtime or process claim. Marking a claim `blocked` or
`out-of-scope` records the disposition but does not silently count it as verified.
The `summary` counts obligations. `by_status`, `by_application_layer`, and
`by_source_state` place each obligation in exactly one bucket.
@@ -264,10 +264,12 @@ describe("reconstruction obligation manifest references", () => {
if (binding === undefined) throw new Error("Expected binding");
const variants: {
readonly code: string;
readonly kind: "positive" | "malformed";
readonly binding: typeof binding;
}[] = [
{
code: "missing-original-case",
kind: "positive",
binding: {
...binding,
original_cases: [
@@ -278,6 +280,7 @@ describe("reconstruction obligation manifest references", () => {
},
{
code: "missing-case",
kind: "positive",
binding: {
...binding,
fixtures: [
@@ -291,17 +294,88 @@ describe("reconstruction obligation manifest references", () => {
],
},
},
{
code: "missing-original-case",
kind: "malformed",
binding: {
...binding,
original_cases: [
...binding.original_cases,
{ kind: "malformed", evidence_id: absent, location: "/old" },
],
},
},
{
code: "missing-case",
kind: "malformed",
binding: {
...binding,
fixtures: [
...binding.fixtures,
{
fixture_id: "packaged.old",
case_kind: "malformed",
authority: "packaged-process",
evidence_ids: [absent],
},
],
},
},
];
for (const { code, binding: dangling } of variants) {
for (const { code, kind, binding: dangling } of variants) {
const open = build({
...ledgerRequest,
manifest: { ...ledgerRequest.manifest, bindings: [dangling] },
});
expect(open.status).not.toBe("ready");
expect(open.obligations[0]?.status).toBe("implemented");
expect(open.obligations[0]?.diagnostics).toEqual([
{ code, detail: expect.stringContaining(absent) },
]);
expect(open.obligations[0]?.diagnostics).toContainEqual({
code,
detail: expect.stringMatching(new RegExp(`${kind}.*${absent}`)),
});
expect(open.evidence_links).toContain(absent);
}
const surplusEvidence = proofEvidence("surplus-case");
const validSurplus = build(
request([...ledgerRequest.evidence_bundle.records, surplusEvidence], {
manifest: {
...ledgerRequest.manifest,
bindings: [
{
...binding,
original_cases: [
...binding.original_cases,
{
kind: "malformed",
evidence_id: surplusEvidence.evidence_id,
location: "/retained",
},
],
fixtures: [
...binding.fixtures,
{
fixture_id: "packaged.malformed",
case_kind: "malformed",
authority: "packaged-process",
evidence_ids: [surplusEvidence.evidence_id],
},
],
},
],
},
}),
);
expect(validSurplus.status).toBe("ready");
expect(validSurplus.obligations[0]?.observed_cases).toContainEqual(
expect.objectContaining({ kind: "malformed", location: "/retained" }),
);
expect(validSurplus.obligations[0]?.binding?.fixtures).toContainEqual(
expect.objectContaining({
case_kind: "malformed",
fixture_id: "packaged.malformed",
}),
);
expect(validSurplus.evidence_links).toContain(surplusEvidence.evidence_id);
});
});
@@ -250,6 +250,16 @@ const evaluateBinding = (
"missing-parser-type",
"This boundary requires an explicit parser, schema, and domain type.",
);
// Optional fixture kinds are retained and linked too, so their Evidence
// references must resolve even though only required kinds close the claim.
for (const fixture of binding.fixtures)
for (const evidenceId of fixture.evidence_ids)
if (!context.evidence.has(evidenceId))
addDiagnostic(
diagnostics,
"missing-case",
`${fixture.case_kind} fixture ${fixture.fixture_id} references Evidence ${evidenceId}, which is absent from the bundle.`,
);
for (const caseKind of candidate.required_case_kinds) {
const fixtures = binding.fixtures.filter(
({ case_kind: fixtureCase }) => fixtureCase === caseKind,
@@ -262,14 +272,6 @@ const evaluateBinding = (
);
continue;
}
for (const fixture of fixtures)
for (const evidenceId of fixture.evidence_ids)
if (!context.evidence.has(evidenceId))
addDiagnostic(
diagnostics,
"missing-case",
`${caseKind} fixture ${fixture.fixture_id} references Evidence ${evidenceId}, which is absent from the bundle.`,
);
if (
!fixtures.some(
(fixture) =>
@@ -36,6 +36,14 @@ export const originalCaseDiagnostics = (
): Diagnostic[] => {
const diagnostics: Diagnostic[] = [];
const originalCases = originalCasesFor(candidate, binding);
// Retain optional case rows, but never leave one of their Evidence links
// unresolved merely because its kind is not required for closure.
for (const { kind, evidence_id: evidenceId } of originalCases)
if (!evidenceById.has(evidenceId))
diagnostics.push({
code: "missing-original-case",
detail: `Original ${kind} case references Evidence ${evidenceId}, which is absent from the bundle.`,
});
for (const caseKind of candidate.required_case_kinds) {
const cases = originalCases.filter(({ kind }) => kind === caseKind);
if (cases.length === 0) {
@@ -45,14 +53,6 @@ export const originalCaseDiagnostics = (
});
continue;
}
// A valid entry for this case kind does not resolve another entry's
// reference; every referenced Evidence ID must be in the bundle.
for (const { evidence_id: evidenceId } of cases)
if (!evidenceById.has(evidenceId))
diagnostics.push({
code: "missing-original-case",
detail: `Original ${caseKind} case references Evidence ${evidenceId}, which is absent from the bundle.`,
});
if (
!cases.some(({ evidence_id: evidenceId }) => {
const evidence = evidenceById.get(evidenceId);