fix(ledger): withhold readiness for unresolved evidence (#7928)

This commit is contained in:
morluto authored and GitHub committed 2026-10-29 07:54:41 +08:00
1 parent a4bd924772
commit 63491645fd
3 files changed
+175

No files matched your search

@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
import {
build,
@@ -8,6 +9,7 @@ import {
processEvidence,
proofEvidence,
readyProcessLedgerRequest,
readyReviewedLedgerRequest,
request,
reviewedObligation,
} from "./ReconstructionObligationLedger.fixture.js";
@@ -256,6 +258,114 @@ describe("reconstruction obligation ledger case closure", () => {
});
describe("reconstruction obligation manifest references", () => {
it("does not claim readiness with unresolved references on an optional obligation", () => {
const input = readyReviewedLedgerRequest();
const reviewed = input.reviewed_obligations[0]!;
const binding = input.manifest.bindings[0]!;
const absent = `ev_${"c".repeat(64)}`;
const original = binding.original_cases[0]!.evidence_id;
const proof = binding.verifier!.result_evidence_id;
const optional = { ...reviewed, required: false };
const candidates = [
{
...binding,
original_cases: [
...binding.original_cases,
{
kind: "malformed" as const,
evidence_id: absent,
location: "/extra",
},
],
},
{
...binding,
fixtures: [
...binding.fixtures,
{
fixture_id: "fixture.extra",
case_kind: "malformed" as const,
authority: "unit" as const,
evidence_ids: [absent],
},
],
},
{
...binding,
verifier: { ...binding.verifier!, result_evidence_id: absent },
},
];
for (const candidate of candidates) {
const result = build({
...input,
reviewed_obligations: [optional],
manifest: { bindings: [candidate], contradictions: [] },
});
expect(result.status).toBe("unknown");
expect(result.coverage.status).toBe("partial");
expect(result.evidence_links).toContain(absent);
expect(result.limitations).toContain(
"The ledger retains 1 Evidence reference(s) absent from the input bundle; closure cannot be claimed.",
);
}
const contradiction = build({
...input,
reviewed_obligations: [optional],
manifest: {
bindings: [binding],
contradictions: [
{
obligation_id: optional.obligation_id,
evidence_ids: [original, absent],
},
],
},
});
expect(contradiction.status).not.toBe("ready");
expect(contradiction.evidence_links).toContain(absent);
const resolved = build({
...input,
reviewed_obligations: [optional],
manifest: {
bindings: [
{
...candidates[1]!,
fixtures: [
...binding.fixtures,
{
fixture_id: "fixture.extra",
case_kind: "malformed",
authority: "unit",
evidence_ids: [proof],
},
],
},
],
contradictions: [],
},
});
expect(resolved.status).toBe("ready");
expect(resolved.obligations[0]?.status).toBe("verified");
const required = readyProcessLedgerRequest();
const mixed = build({
evidence_bundle: createEvidenceBundle([
...input.evidence_bundle.records,
...required.evidence_bundle.records,
]),
reviewed_obligations: [optional],
manifest: {
bindings: [candidates[1]!, ...required.manifest.bindings],
contradictions: [],
},
});
expect(mixed.summary.required_open).toBe(0);
expect(mixed.status).toBe("unknown");
expect(mixed.evidence_links).toContain(absent);
});
});
describe("reconstruction obligation manifest reference controls", () => {
it("withholds readiness when a manifest entry names absent Evidence", () => {
const ledgerRequest = readyProcessLedgerRequest();
// A valid entry for the same case kind must not hide an absent reference.
@@ -80,6 +80,7 @@ export const evaluateReconstructionObligationLedger = ({
generationLimitations,
candidates.length,
orphanManifestIds,
evidenceLinks.filter((id) => !records.has(id)).length,
);
const coverage = reconstructionObligationCoverage(limitations);
const semantic = {
@@ -158,6 +159,7 @@ const ledgerLimitations = (
generationLimitations: readonly string[],
candidateCount: number,
orphanManifestIds: readonly string[],
missingEvidenceCount: number,
): string[] =>
[
...new Set([
@@ -172,6 +174,11 @@ const ledgerLimitations = (
`Manifest entries reference unknown obligations: ${orphanManifestIds.join(", ")}.`,
]
: []),
...(missingEvidenceCount > 0
? [
`The ledger retains ${String(missingEvidenceCount)} Evidence reference(s) absent from the input bundle; closure cannot be claimed.`,
]
: []),
]),
].sort();
@@ -17,11 +17,69 @@ import {
MIXED_AUTHORITY_SUMMARY,
mixedAuthorityRequest,
readyProcessLedgerRequest,
readyReviewedLedgerRequest,
} from "../../../src/application/ReconstructionObligationLedger.fixture.js";
const execute = promisify(execFile);
describe("reconstruction obligation ledger CLI parity", () => {
it("withholds readiness for an optional obligation's missing Evidence", async () => {
const input = readyReviewedLedgerRequest();
const binding = input.manifest.bindings[0]!;
const absent = `ev_${"c".repeat(64)}`;
const extra = {
fixture_id: "fixture.extra",
case_kind: "malformed" as const,
authority: "unit" as const,
evidence_ids: [absent],
};
for (const evidenceId of [absent, binding.verifier!.result_evidence_id]) {
const variant = {
...input,
reviewed_obligations: [
{ ...input.reviewed_obligations[0]!, required: false },
],
manifest: {
...input.manifest,
bindings: [
{
...binding,
fixtures: [
...binding.fixtures,
{ ...extra, evidence_ids: [evidenceId] },
],
},
],
},
};
const { stdout } = await execute(
process.execPath,
[
"scripts/rea.mjs",
"build-reconstruction-obligation-ledger",
JSON.stringify(variant),
"--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(evidenceId === absent ? "unknown" : "ready");
if (evidenceId === absent) {
expect(ledger.evidence_links).toContain(absent);
expect(ledger.obligations[0]?.diagnostics).toContainEqual(
expect.objectContaining({
code: "missing-case",
detail: expect.stringContaining(absent),
}),
);
}
}
}, 20_000);
});
describe("reconstruction obligation ledger CLI bundle validation", () => {
it("reports non-canonical authenticated bundles as caller input", async () => {
const records = ["first", "second"].map((label) =>
createEvidence(