[GH-ISSUE #6770] [Bug] Non-required case kinds leave dangling Evidence references in ready obligation ledgers #4128

Closed
opened 2026-10-31 00:47:00 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 23, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6770

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

The production buildReconstructionObligationLedgerEvidence service returns a ready ledger with a verified obligation and no diagnostics when an additional original case or fixture uses a valid but non-required case kind and cites Evidence absent from the input bundle. The missing ID remains in the ledger's evidence_links.

In the reduced fixture, only positive is required. Adding an absent positive original case correctly produces missing-original-case; adding an absent positive fixture correctly produces missing-case. Changing only that extra entry's kind to malformed makes the missing-reference diagnostic disappear and restores ready. Both arrays exhibit the same skipped-validation boundary.

Reproduction

Observed once for each of five variants through the production application service, using authenticated synthetic Evidence. From the source checkout root, save the following complete reduced fixture as /tmp/rea-ledger-probe.mjs, then run node --import tsx /tmp/rea-ledger-probe.mjs . with the repository's locked dependencies installed. No target or declared verifier command executes; the fixture creates data and calls the service directly.

import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
const repo = resolve(process.argv[2] ?? '.');
const load = p => import(pathToFileURL(resolve(repo, 'src', p)).href);
const { createEvidence } = await load('domain/evidence.ts');
const { createEvidenceBundle } = await load('domain/evidenceBundle.ts');
const { buildReconstructionObligationLedgerEvidence: build } =
  await load('application/ReconstructionObligationLedgerService.ts');
const id = 'sample.static';
const proof = createEvidence(undefined, { id:'fixture', name:'Fixture', version:'1' }, {
  predicateType:'rea.reconstruction-proof', operation:'verify_reconstruction_obligations',
  parameters:{}, result:{ passed:true, obligation_ids:[id], fixture_ids:['sample.positive'],
    case_kinds:['positive'], verifier_ids:['sample.verifier'], claim_ids:['sample.claim'] },
  confidence:'observed', authority:'shipped-artifact'
});
const obligation = {
  obligation_id:id, obligation_version:1, title:'Static sample', application_layer:'other',
  family:'sample', target:{artifact_sha256:null, application_node_id:null, semantic_node_id:null, location:'sample'},
  required:true, required_case_kinds:['positive'], required_original_authority:'static',
  required_fixture_authority:'unit', required_verifier_authority:'unit', requires_parser_type:false,
  dependency_obligation_ids:[], residual_unknown_ids:[], unavailable_authority:[],
  required_next_evidence:[], disposition:'active', review_evidence_ids:[proof.evidence_id]
};
const binding = {
  obligation_id:id, owner:{module_path:'sample.ts', symbol:'sample', owner_sha256:'1'.repeat(64)},
  parser_type:null, original_cases:[{kind:'positive',evidence_id:proof.evidence_id,location:'/positive'}],
  fixtures:[{fixture_id:'sample.positive',case_kind:'positive',authority:'unit',evidence_ids:[proof.evidence_id]}],
  verifier:{verifier_id:'sample.verifier',claim_id:'sample.claim',command:'node sample-verifier.mjs',
    authority:'unit',status:'pass',result_evidence_id:proof.evidence_id,enumerated_obligation_ids:[id],
    nondeterminism:{mode:'total-order',specification:'One static assertion.'}}
};
const absent = 'ev_'+'a'.repeat(64);
for (const [kind, field] of [['control','none'],['positive','original'],['positive','fixture'],['malformed','original'],['malformed','fixture']]) {
  const changed = structuredClone(binding);
  if (field === 'original') changed.original_cases.push({kind,evidence_id:absent,location:'/absent'});
  if (field === 'fixture') changed.fixtures.push({fixture_id:'extra',case_kind:kind,authority:'unit',evidence_ids:[absent]});
  const result = build({evidence_bundle:createEvidenceBundle([proof]),reviewed_obligations:[obligation],manifest:{bindings:[changed],contradictions:[]}});
  if (!result.ok) throw result.error;
  const ledger = result.value.normalized_result;
  console.log(JSON.stringify({kind,field,status:ledger.status,obligation:ledger.obligations[0].status,
    missingLink:ledger.evidence_links.includes(absent),diagnostics:ledger.obligations[0].diagnostics.map(d=>d.code)}));
}

Expected behavior

Every retained manifest Evidence reference must resolve, including references in case kinds beyond the required minimum. The ledger guide explicitly requires all referenced Evidence IDs to exist in the bundle. Its requirement is not restricted to required case kinds.

The complete control should stay ready. Either added entry citing the absent ID should reject the request or produce a diagnostic and withhold readiness. Changing a case label must not disable reference validation while retaining the citation in the result.

Impact and scope

A manifest retaining an obsolete optional observation or fixture receives a clean closure verdict with incomplete provenance. The output contains a citation that cannot resolve against its supplied bundle. This is a bounded analysis-result defect; no loss of stored Evidence or downstream deployment incident was observed.

Workaround: check every original-case and fixture reference against the bundle before submitting the manifest, then restore the missing record or remove the obsolete entry. The complete control demonstrates the latter path. CLI/MCP transport behavior, other case-kind combinations, and real-world frequency were not exercised.

REA version

Main source commit 5f702dd6ccd9e8e505e224854b91678439c3b41f, package rea-agents version 6.7.0. The released rea-agents-6.7.0 tag was fetched but its runtime was not tested.

Runtime and platform

Node.js v24.22.1; Linux x86_64; repository-locked tsx source loader. No external analysis provider is involved.

Target and artifact context

One synthetic shipped-artifact Evidence record carrying a bound static proof, one reviewed obligation requiring a positive case, and one manifest binding. The absent record ID is ev_ followed by 64 a characters. No binary, application, network target, or verifier executes.

Interface

Other

Earliest failure stage

Request parsing or validation

Relevant output or evidence

Actual stdout from the five service calls above; the process exited zero:

{"kind":"control","field":"none","status":"ready","obligation":"verified","missingLink":false,"diagnostics":[]}
{"kind":"positive","field":"original","status":"open","obligation":"implemented","missingLink":true,"diagnostics":["missing-original-case"]}
{"kind":"positive","field":"fixture","status":"open","obligation":"implemented","missingLink":true,"diagnostics":["missing-case"]}
{"kind":"malformed","field":"original","status":"ready","obligation":"verified","missingLink":true,"diagnostics":[]}
{"kind":"malformed","field":"fixture","status":"ready","obligation":"verified","missingLink":true,"diagnostics":[]}

Evidence provenance: observed production application-boundary calls, with source-derived explanation below. Local verification: reproduced. Reproduction completeness: complete for the service boundary; CLI/MCP adapters were not run.

Typed outcome or recovery expected

A typed invalid-input result or an explicit unresolved-reference diagnostic that withholds ready for both malformed variants. Keep the complete control ready and retain the existing positive controls' missing-reference diagnostics.

Analysis or suspected cause

Source-derived: both existence checks sit inside loops over candidate.required_case_kinds. The schemas accept additional valid case kinds. An extra malformed entry is therefore serialized into the obligation and evidence-link projection without ever reaching either existence check. The observed paired controls isolate the skipped kind filter from Evidence authentication and proof authority.

Additional context

Closed #3363 reported a valid entry hiding an absent reference in another entry of the same required kind. That path is fixed in the observed positive controls. This report isolates the remaining branch: an extra, non-required kind receives no reference checks at all. It is a follow-up to that fix, rather than a claim that its original same-kind reproduction still fails.

The supplied open-issue snapshot and targeted open/closed searches contained no report of this non-required-kind trigger. Closed #5925 concerned grouping contradiction entries, and #4396 requests verifier negative controls; neither concerns validation of surplus manifest references. The submitting owner repeated the open/closed searches immediately before filing.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The executed paired controls show a ready, verified result retaining an unresolvable Evidence citation when only the extra entry's case kind changes; this breaks the documented all-references invariant in a bounded workflow.
  • Workaround status: available
  • Workaround basis: Validate all manifest IDs against the supplied bundle and restore missing records or remove obsolete entries; the executed complete control returns ready without the dangling citation.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6770 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior The production `buildReconstructionObligationLedgerEvidence` service returns a `ready` ledger with a `verified` obligation and no diagnostics when an additional original case or fixture uses a valid but non-required case kind and cites Evidence absent from the input bundle. The missing ID remains in the ledger's `evidence_links`. In the reduced fixture, only `positive` is required. Adding an absent `positive` original case correctly produces `missing-original-case`; adding an absent `positive` fixture correctly produces `missing-case`. Changing only that extra entry's kind to `malformed` makes the missing-reference diagnostic disappear and restores `ready`. Both arrays exhibit the same skipped-validation boundary. ### Reproduction Observed once for each of five variants through the production application service, using authenticated synthetic Evidence. From the source checkout root, save the following complete reduced fixture as `/tmp/rea-ledger-probe.mjs`, then run `node --import tsx /tmp/rea-ledger-probe.mjs .` with the repository's locked dependencies installed. No target or declared verifier command executes; the fixture creates data and calls the service directly. ```js import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const repo = resolve(process.argv[2] ?? '.'); const load = p => import(pathToFileURL(resolve(repo, 'src', p)).href); const { createEvidence } = await load('domain/evidence.ts'); const { createEvidenceBundle } = await load('domain/evidenceBundle.ts'); const { buildReconstructionObligationLedgerEvidence: build } = await load('application/ReconstructionObligationLedgerService.ts'); const id = 'sample.static'; const proof = createEvidence(undefined, { id:'fixture', name:'Fixture', version:'1' }, { predicateType:'rea.reconstruction-proof', operation:'verify_reconstruction_obligations', parameters:{}, result:{ passed:true, obligation_ids:[id], fixture_ids:['sample.positive'], case_kinds:['positive'], verifier_ids:['sample.verifier'], claim_ids:['sample.claim'] }, confidence:'observed', authority:'shipped-artifact' }); const obligation = { obligation_id:id, obligation_version:1, title:'Static sample', application_layer:'other', family:'sample', target:{artifact_sha256:null, application_node_id:null, semantic_node_id:null, location:'sample'}, required:true, required_case_kinds:['positive'], required_original_authority:'static', required_fixture_authority:'unit', required_verifier_authority:'unit', requires_parser_type:false, dependency_obligation_ids:[], residual_unknown_ids:[], unavailable_authority:[], required_next_evidence:[], disposition:'active', review_evidence_ids:[proof.evidence_id] }; const binding = { obligation_id:id, owner:{module_path:'sample.ts', symbol:'sample', owner_sha256:'1'.repeat(64)}, parser_type:null, original_cases:[{kind:'positive',evidence_id:proof.evidence_id,location:'/positive'}], fixtures:[{fixture_id:'sample.positive',case_kind:'positive',authority:'unit',evidence_ids:[proof.evidence_id]}], verifier:{verifier_id:'sample.verifier',claim_id:'sample.claim',command:'node sample-verifier.mjs', authority:'unit',status:'pass',result_evidence_id:proof.evidence_id,enumerated_obligation_ids:[id], nondeterminism:{mode:'total-order',specification:'One static assertion.'}} }; const absent = 'ev_'+'a'.repeat(64); for (const [kind, field] of [['control','none'],['positive','original'],['positive','fixture'],['malformed','original'],['malformed','fixture']]) { const changed = structuredClone(binding); if (field === 'original') changed.original_cases.push({kind,evidence_id:absent,location:'/absent'}); if (field === 'fixture') changed.fixtures.push({fixture_id:'extra',case_kind:kind,authority:'unit',evidence_ids:[absent]}); const result = build({evidence_bundle:createEvidenceBundle([proof]),reviewed_obligations:[obligation],manifest:{bindings:[changed],contradictions:[]}}); if (!result.ok) throw result.error; const ledger = result.value.normalized_result; console.log(JSON.stringify({kind,field,status:ledger.status,obligation:ledger.obligations[0].status, missingLink:ledger.evidence_links.includes(absent),diagnostics:ledger.obligations[0].diagnostics.map(d=>d.code)})); } ``` ### Expected behavior Every retained manifest Evidence reference must resolve, including references in case kinds beyond the required minimum. The [ledger guide](https://github.com/morluto/rea/blob/5f702dd6ccd9e8e505e224854b91678439c3b41f/docs/reconstruction-obligation-ledgers.md#L318-L321) explicitly requires all referenced Evidence IDs to exist in the bundle. Its requirement is not restricted to required case kinds. The complete control should stay `ready`. Either added entry citing the absent ID should reject the request or produce a diagnostic and withhold readiness. Changing a case label must not disable reference validation while retaining the citation in the result. ### Impact and scope A manifest retaining an obsolete optional observation or fixture receives a clean closure verdict with incomplete provenance. The output contains a citation that cannot resolve against its supplied bundle. This is a bounded analysis-result defect; no loss of stored Evidence or downstream deployment incident was observed. Workaround: check every original-case and fixture reference against the bundle before submitting the manifest, then restore the missing record or remove the obsolete entry. The complete control demonstrates the latter path. CLI/MCP transport behavior, other case-kind combinations, and real-world frequency were not exercised. ### REA version Main source commit `5f702dd6ccd9e8e505e224854b91678439c3b41f`, package `rea-agents` version `6.7.0`. The released `rea-agents-6.7.0` tag was fetched but its runtime was not tested. ### Runtime and platform Node.js `v24.22.1`; Linux x86_64; repository-locked `tsx` source loader. No external analysis provider is involved. ### Target and artifact context One synthetic shipped-artifact Evidence record carrying a bound static proof, one reviewed obligation requiring a positive case, and one manifest binding. The absent record ID is `ev_` followed by 64 `a` characters. No binary, application, network target, or verifier executes. ### Interface Other ### Earliest failure stage Request parsing or validation ### Relevant output or evidence Actual stdout from the five service calls above; the process exited zero: ```json {"kind":"control","field":"none","status":"ready","obligation":"verified","missingLink":false,"diagnostics":[]} {"kind":"positive","field":"original","status":"open","obligation":"implemented","missingLink":true,"diagnostics":["missing-original-case"]} {"kind":"positive","field":"fixture","status":"open","obligation":"implemented","missingLink":true,"diagnostics":["missing-case"]} {"kind":"malformed","field":"original","status":"ready","obligation":"verified","missingLink":true,"diagnostics":[]} {"kind":"malformed","field":"fixture","status":"ready","obligation":"verified","missingLink":true,"diagnostics":[]} ``` Evidence provenance: observed production application-boundary calls, with source-derived explanation below. Local verification: reproduced. Reproduction completeness: complete for the service boundary; CLI/MCP adapters were not run. - [Original-case validation](https://github.com/morluto/rea/blob/5f702dd6ccd9e8e505e224854b91678439c3b41f/src/application/ReconstructionObligationOriginalCases.ts#L38-L55) checks references only after filtering to each required case kind. - [Fixture validation](https://github.com/morluto/rea/blob/5f702dd6ccd9e8e505e224854b91678439c3b41f/src/application/ReconstructionObligationLedgerEvaluation.ts#L253-L272) applies the same required-kind filter before checking Evidence existence. - [Evidence-link projection](https://github.com/morluto/rea/blob/5f702dd6ccd9e8e505e224854b91678439c3b41f/src/application/ReconstructionObligationLedgerProjection.ts#L14-L20) nevertheless includes references from every original case and fixture. - [Existing regression test](https://github.com/morluto/rea/blob/5f702dd6ccd9e8e505e224854b91678439c3b41f/src/application/ReconstructionObligationLedger.closure.test.ts#L259-L317) requires missing references to prevent readiness, but uses a required case kind. ### Typed outcome or recovery expected A typed invalid-input result or an explicit unresolved-reference diagnostic that withholds `ready` for both `malformed` variants. Keep the complete control ready and retain the existing `positive` controls' missing-reference diagnostics. ### Analysis or suspected cause Source-derived: both existence checks sit inside loops over `candidate.required_case_kinds`. The schemas accept additional valid case kinds. An extra `malformed` entry is therefore serialized into the obligation and evidence-link projection without ever reaching either existence check. The observed paired controls isolate the skipped kind filter from Evidence authentication and proof authority. ### Additional context [Closed #3363](https://github.com/morluto/rea/issues/3363) reported a valid entry hiding an absent reference in another entry of the same required kind. That path is fixed in the observed `positive` controls. This report isolates the remaining branch: an extra, non-required kind receives no reference checks at all. It is a follow-up to that fix, rather than a claim that its original same-kind reproduction still fails. The supplied open-issue snapshot and targeted open/closed searches contained no report of this non-required-kind trigger. [Closed #5925](https://github.com/morluto/rea/issues/5925) concerned grouping contradiction entries, and [#4396](https://github.com/morluto/rea/issues/4396) requests verifier negative controls; neither concerns validation of surplus manifest references. The submitting owner repeated the open/closed searches immediately before filing. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The executed paired controls show a ready, verified result retaining an unresolvable Evidence citation when only the extra entry's case kind changes; this breaks the documented all-references invariant in a bounded workflow. - Workaround status: available - Workaround basis: Validate all manifest IDs against the supplied bundle and restore missing records or remove obsolete entries; the executed complete control returns ready without the dangling citation.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4128
No description provided.