[GH-ISSUE #5925] [Bug] Missing Evidence in one contradiction entry hides a complete contradiction #3602

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

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

Originally assigned to: @kaoru0822-kitauji on GitHub.

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

rea build-reconstruction-obligation-ledger loses a complete contradiction when a second contradiction entry for the same obligation references missing Evidence. The complete entry alone produces obligation status contradicted, ledger status failed, and the obligation in reports.contradicted_obligation_ids. Adding the incomplete entry, in either order, changes these to implemented, open, and an empty contradiction report. Only contradiction-evidence-missing remains.

I executed the public CLI for the control and all three contradiction variants. Every CLI call succeeded. This does not establish false readiness: the mixed cases remain open, but no longer report the complete contradiction.

Reproduction

Use an installed, built checkout of the revision below. Save this temporary test as src/application/contradiction-repro.test.ts. It uses authenticated synthetic Evidence from the existing packaged-process fixture helpers and executes four real CLI subprocesses. No process capture or external analysis provider is mocked or invoked. JavaScript Unicode escapes in identifiers and strings preserve their ordinary values.

import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { expect, it } from "vitest";
import { boundProofEvidence, build, originalCases, processEvidence, request } from "./ReconstructionObligationLedger.fixture.js";
const execute = promisify(execFile);
/** Build a request whose packaged-process obligation closes as ready. */
const readyProcessLedgerRequest = () => {
  const capture = processEvidence();
  const initial = build(request([capture]));
  const obligation = initial.obligations[0];
  if (obligation === undefined) throw new Error("Expected process obligation");
  const fixtureIds = obligation.required_case_kinds.map(
    (caseKind) => `packaged.${caseKind}`,
  );
  const proof = boundProofEvidence("packaged", {
    obligation_ids: [obligation.obligation_id],
    fixture_ids: fixtureIds,
    case_kinds: obligation.required_case_kinds,
    ver\u0069fier_ids: ["ver\u0069fier.packaged"],
    claim_ids: ["claim.process"],
  });
  const ledgerRequest = request([capture, proof], {
    manifest: {
      bindings: [
        {
          obligation_id: obligation.obligation_id,
          owner: {
            module_path: "src/processOwner.ts",
            symbol: "runProcess",
            owner_sha256: "2".repeat(64),
          },
          parser_type: null,
          original_cases: originalCases(obligation, capture.evidence_id),
          fixtures: obligation.required_case_kinds.map((caseKind) => ({
            fixture_id: `packaged.${caseKind}`,
            case_kind: caseKind,
            authority: "packaged-process",
            evidence_ids: [proof.evidence_id],
          })),
          ver\u0069fier: {
            ver\u0069fier_id: "ver\u0069fier.packaged",
            claim_id: "claim.process",
            command: "npm run verify:package -- process",
            authority: "packaged-process",
            status: "pass",
            result_evidence_id: proof.evidence_id,
            enumerated_obligation_ids: [obligation.obligation_id],
            nondeterminism: {
              mode: "partial-order",
              specification:
                "Startup precedes teardown; worker completion is unordered.",
            },
          },
        },
      ],
      contradictions: [],
    },
  });
  return ledgerRequest;
};


it("preserves a complete contradiction alongside an incomplete entry", async () => {
  const input = readyProcessLedgerRequest();
  const id = input.manifest.bindings[0]!.obligation_id;
  const ids = input.evidence_bundle.records.map(record => record.evidence_id);
  const complete = { obligation_id: id, evidence_ids: ids };
  const incomplete = { obligation_id: id, evidence_ids: [ids[0]!, `ev_${"f".repeat(64)}`] };
  const results = [];
  for (const [label, contradictions] of [
    ["none", []],
    ["complete", [complete]],
    ["complete,incomplete", [complete, incomplete]],
    ["incomplete,complete", [incomplete, complete]],
  ] as const) {
    const { stdout } = await execute(process.execPath, [
      "scripts/rea.mjs", "build-reconstruction-obligation-ledger",
      JSON.stringify({ ...input, manifest: { ...input.manifest, contradictions } }), "--json"
    ], { cwd: process.cwd(), env: process.env, maxBuffer: 16 * 1024 * 1024 });
    const ledger = JSON.parse(stdout).normalized_result;
    const result = { label, status: ledger.status, obligation: ledger.obligations[0].status,
      diagnostics: ledger.obligations[0].diagnostics.map((d: {code: string}) => d.code),
      contradicted: ledger.reports.contradicted_obligation_ids };
    console.log(JSON.stringify(result));
    results.push(result);
  }
  expect(results[0].obligation).toBe("verified");
  expect(results[1].obligation).toBe("contradicted");
  expect(results[1].status).toBe("failed");
  expect(results[1].contradicted).toEqual([id]);
  for (const result of results.slice(2)) {
    expect.soft(result.diagnostics).toContain("contradiction");
    expect.soft(result.diagnostics).toContain("contradiction-evidence-missing");
    expect.soft(result.obligation).toBe("contradicted");
    expect.soft(result.status).toBe("failed");
    expect.soft(result.contradicted).toEqual([id]);
  }
}, 60000);

Run only this file:

node node_modules/vitest/vitest.mjs run src/application/contradiction-repro.test.ts --maxWorkers 1

The control assertions pass. Both mixed cases fail the assertions for the contradiction diagnostic, obligation status, ledger status, and contradiction report. Their missing-Evidence assertions pass. Delete the temporary test afterward. The comparison reproduced in two focused runs; each ran the four public CLI commands.

Expected behavior

Adding an incomplete contradiction entry must retain the already complete contradiction for the same obligation. The mixed input should produce both contradiction and contradiction-evidence-missing, preserve status contradicted and the report entry, and keep the ledger failed.

The manifest schema represents contradictions as separate entries and permits repeated obligation IDs. The existing complete-contradiction test requires failed, contradicted, and the report entry. The ledger guide describes conservative Evidence-backed closure with explicit contradictions. Missing Evidence in another entry should not erase those established facts.

Impact and scope

P2: callers retaining a complete contradiction and a stale incomplete entry receive incorrect conflict diagnostics, obligation status, and the contradiction report. Closure remains open, so the observed defect is the loss of the conflict classification rather than a ready decision. No stored Evidence loss or security boundary crossing was observed.

A practical workaround is to restore the absent Evidence record or remove the obsolete incomplete entry while retaining the complete contradiction. The executed complete-only control confirms the latter restores the failed/contradicted result and report entry. Production frequency is unknown. CLI behavior was executed; the MCP tool reaches the same service according to source inspection but was not executed.

REA version

rea-agents 6.7.0; source revision 5241597594

Runtime and platform

Node.js v26.12.0; macOS 27.0.1 arm64; Vitest 5.0.3 with preinstalled locked dependencies and built runtime. The repository pins Node 24.18.0; that runtime was not tested.

Target and artifact context

Authenticated synthetic Evidence bundle containing a canonical process-capture fixture and bound reconstruction proof. One generated required packaged-process obligation has complete owner, case, fixture, and proof bindings. No native target or external analysis provider is involved.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Trimmed JSON printed from the actual public CLI responses:

{"label":"none","status":"ready","obligation":"verified","diagnostics":[],"contradicted":[]}
{"label":"complete","status":"failed","obligation":"contradicted","diagnostics":["contradiction"],"contradicted":["obl_be8ff3f3dab6e4b8c59b69d5e6b05afd9f4aefead3323f14a4cf87134250fb51"]}
{"label":"complete,incomplete","status":"open","obligation":"implemented","diagnostics":["contradiction-evidence-missing"],"contradicted":[]}
{"label":"incomplete,complete","status":"open","obligation":"implemented","diagnostics":["contradiction-evidence-missing"],"contradicted":[]}

The focused test reports one failing test with eight soft assertion failures across the two mixed cases. The complete-only and no-contradiction controls pass their assertions.

Typed outcome or recovery expected

Return both contradiction facts for the mixed input: the complete conflict and the incomplete entry's missing Evidence. Retain the obligation in the contradiction report and classify its closure as failed. Keep the control ready and the complete-only case failed.

Analysis or suspected cause

Source-derived: grouping concatenates every contradiction entry's Evidence IDs for one obligation. The diagnostic function then chooses only the missing-Evidence branch whenever any flattened ID is absent. Entry boundaries have been lost, so a complete entry cannot produce its own contradiction diagnostic.

The status selector maps the remaining diagnostic to implemented. The ledger status and report selectors then produce open and an empty contradiction report. Preserving each contradiction entry while evaluating its Evidence would allow both facts to survive.

Additional context

Related reports checked: #5838 concerns authority-summary grouping; #3370 concerns semantic unknown propagation; #3363 concerns original-case and fixture missing references despite readiness. The latter's fix does not address contradiction-entry grouping. Reports #3889 and #3937 and PR #4337 concern the separate reconstruction-coverage evaluator.

A freshness check fetched upstream main at bcfcd8b6e2ee0c44c7ac656688db5ceb61a847d5. The ledger application/domain sources, CLI/MCP registrations, and guide are unchanged from the tested baseline at that revision.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5925 Originally assigned to: @kaoru0822-kitauji on GitHub. ### 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 `rea build-reconstruction-obligation-ledger` loses a complete contradiction when a second contradiction entry for the same obligation references missing Evidence. The complete entry alone produces obligation status `contradicted`, ledger status `failed`, and the obligation in `reports.contradicted_obligation_ids`. Adding the incomplete entry, in either order, changes these to `implemented`, `open`, and an empty contradiction report. Only `contradiction-evidence-missing` remains. I executed the public CLI for the control and all three contradiction variants. Every CLI call succeeded. This does not establish false readiness: the mixed cases remain open, but no longer report the complete contradiction. ### Reproduction Use an installed, built checkout of the revision below. Save this temporary test as `src/application/contradiction-repro.test.ts`. It uses authenticated synthetic Evidence from the existing packaged-process fixture helpers and executes four real CLI subprocesses. No process capture or external analysis provider is mocked or invoked. JavaScript Unicode escapes in identifiers and strings preserve their ordinary values. ```ts import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { expect, it } from "vitest"; import { boundProofEvidence, build, originalCases, processEvidence, request } from "./ReconstructionObligationLedger.fixture.js"; const execute = promisify(execFile); /** Build a request whose packaged-process obligation closes as ready. */ const readyProcessLedgerRequest = () => { const capture = processEvidence(); const initial = build(request([capture])); const obligation = initial.obligations[0]; if (obligation === undefined) throw new Error("Expected process obligation"); const fixtureIds = obligation.required_case_kinds.map( (caseKind) => `packaged.${caseKind}`, ); const proof = boundProofEvidence("packaged", { obligation_ids: [obligation.obligation_id], fixture_ids: fixtureIds, case_kinds: obligation.required_case_kinds, ver\u0069fier_ids: ["ver\u0069fier.packaged"], claim_ids: ["claim.process"], }); const ledgerRequest = request([capture, proof], { manifest: { bindings: [ { obligation_id: obligation.obligation_id, owner: { module_path: "src/processOwner.ts", symbol: "runProcess", owner_sha256: "2".repeat(64), }, parser_type: null, original_cases: originalCases(obligation, capture.evidence_id), fixtures: obligation.required_case_kinds.map((caseKind) => ({ fixture_id: `packaged.${caseKind}`, case_kind: caseKind, authority: "packaged-process", evidence_ids: [proof.evidence_id], })), ver\u0069fier: { ver\u0069fier_id: "ver\u0069fier.packaged", claim_id: "claim.process", command: "npm run verify:package -- process", authority: "packaged-process", status: "pass", result_evidence_id: proof.evidence_id, enumerated_obligation_ids: [obligation.obligation_id], nondeterminism: { mode: "partial-order", specification: "Startup precedes teardown; worker completion is unordered.", }, }, }, ], contradictions: [], }, }); return ledgerRequest; }; it("preserves a complete contradiction alongside an incomplete entry", async () => { const input = readyProcessLedgerRequest(); const id = input.manifest.bindings[0]!.obligation_id; const ids = input.evidence_bundle.records.map(record => record.evidence_id); const complete = { obligation_id: id, evidence_ids: ids }; const incomplete = { obligation_id: id, evidence_ids: [ids[0]!, `ev_${"f".repeat(64)}`] }; const results = []; for (const [label, contradictions] of [ ["none", []], ["complete", [complete]], ["complete,incomplete", [complete, incomplete]], ["incomplete,complete", [incomplete, complete]], ] as const) { const { stdout } = await execute(process.execPath, [ "scripts/rea.mjs", "build-reconstruction-obligation-ledger", JSON.stringify({ ...input, manifest: { ...input.manifest, contradictions } }), "--json" ], { cwd: process.cwd(), env: process.env, maxBuffer: 16 * 1024 * 1024 }); const ledger = JSON.parse(stdout).normalized_result; const result = { label, status: ledger.status, obligation: ledger.obligations[0].status, diagnostics: ledger.obligations[0].diagnostics.map((d: {code: string}) => d.code), contradicted: ledger.reports.contradicted_obligation_ids }; console.log(JSON.stringify(result)); results.push(result); } expect(results[0].obligation).toBe("verified"); expect(results[1].obligation).toBe("contradicted"); expect(results[1].status).toBe("failed"); expect(results[1].contradicted).toEqual([id]); for (const result of results.slice(2)) { expect.soft(result.diagnostics).toContain("contradiction"); expect.soft(result.diagnostics).toContain("contradiction-evidence-missing"); expect.soft(result.obligation).toBe("contradicted"); expect.soft(result.status).toBe("failed"); expect.soft(result.contradicted).toEqual([id]); } }, 60000); ``` Run only this file: ```sh node node_modules/vitest/vitest.mjs run src/application/contradiction-repro.test.ts --maxWorkers 1 ``` The control assertions pass. Both mixed cases fail the assertions for the contradiction diagnostic, obligation status, ledger status, and contradiction report. Their missing-Evidence assertions pass. Delete the temporary test afterward. The comparison reproduced in two focused runs; each ran the four public CLI commands. ### Expected behavior Adding an incomplete contradiction entry must retain the already complete contradiction for the same obligation. The mixed input should produce both `contradiction` and `contradiction-evidence-missing`, preserve status `contradicted` and the report entry, and keep the ledger `failed`. The [manifest schema](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/reconstructionObligationLedgerSchemas.ts#L170-L178) represents contradictions as separate entries and permits repeated obligation IDs. The [existing complete-contradiction test](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/ReconstructionObligationLedger.authority.test.ts#L197-L219) requires `failed`, `contradicted`, and the report entry. The [ledger guide](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/reconstruction-obligation-ledgers.md#L3-L18) describes conservative Evidence-backed closure with explicit contradictions. Missing Evidence in another entry should not erase those established facts. ### Impact and scope P2: callers retaining a complete contradiction and a stale incomplete entry receive incorrect conflict diagnostics, obligation status, and the contradiction report. Closure remains open, so the observed defect is the loss of the conflict classification rather than a ready decision. No stored Evidence loss or security boundary crossing was observed. A practical workaround is to restore the absent Evidence record or remove the obsolete incomplete entry while retaining the complete contradiction. The executed complete-only control confirms the latter restores the failed/contradicted result and report entry. Production frequency is unknown. CLI behavior was executed; the MCP tool reaches the same service according to source inspection but was not executed. ### REA version rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e ### Runtime and platform Node.js v26.12.0; macOS 27.0.1 arm64; Vitest 5.0.3 with preinstalled locked dependencies and built runtime. The repository pins Node 24.18.0; that runtime was not tested. ### Target and artifact context Authenticated synthetic Evidence bundle containing a canonical process-capture fixture and bound reconstruction proof. One generated required packaged-process obligation has complete owner, case, fixture, and proof bindings. No native target or external analysis provider is involved. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Trimmed JSON printed from the actual public CLI responses: ```json {"label":"none","status":"ready","obligation":"verified","diagnostics":[],"contradicted":[]} {"label":"complete","status":"failed","obligation":"contradicted","diagnostics":["contradiction"],"contradicted":["obl_be8ff3f3dab6e4b8c59b69d5e6b05afd9f4aefead3323f14a4cf87134250fb51"]} {"label":"complete,incomplete","status":"open","obligation":"implemented","diagnostics":["contradiction-evidence-missing"],"contradicted":[]} {"label":"incomplete,complete","status":"open","obligation":"implemented","diagnostics":["contradiction-evidence-missing"],"contradicted":[]} ``` The focused test reports one failing test with eight soft assertion failures across the two mixed cases. The complete-only and no-contradiction controls pass their assertions. ### Typed outcome or recovery expected Return both contradiction facts for the mixed input: the complete conflict and the incomplete entry's missing Evidence. Retain the obligation in the contradiction report and classify its closure as failed. Keep the control ready and the complete-only case failed. ### Analysis or suspected cause Source-derived: [grouping](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/ReconstructionObligationLedgerDiagnostics.ts#L23-L33) concatenates every contradiction entry's Evidence IDs for one obligation. The [diagnostic function](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/ReconstructionObligationLedgerDiagnostics.ts#L60-L84) then chooses only the missing-Evidence branch whenever any flattened ID is absent. Entry boundaries have been lost, so a complete entry cannot produce its own contradiction diagnostic. The [status selector](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/ReconstructionObligationLedgerEvaluation.ts#L452-L469) maps the remaining diagnostic to `implemented`. The [ledger status and report selectors](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/reconstructionObligationLedgerIntegrity.ts#L58-L94) then produce `open` and an empty contradiction report. Preserving each contradiction entry while evaluating its Evidence would allow both facts to survive. ### Additional context Related reports checked: #5838 concerns authority-summary grouping; #3370 concerns semantic unknown propagation; #3363 concerns original-case and fixture missing references despite readiness. The latter's fix does not address contradiction-entry grouping. Reports #3889 and #3937 and PR #4337 concern the separate reconstruction-coverage evaluator. A freshness check fetched upstream main at `bcfcd8b6e2ee0c44c7ac656688db5ceb61a847d5`. The ledger application/domain sources, CLI/MCP registrations, and guide are unchanged from the tested baseline at that revision.
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#3602
No description provided.