mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user