[GH-ISSUE #7373] [Bug] Process comparison reports definite results for unknown filesystem effects #4381

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

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

Before submitting

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

Area

Process capture or runtime observation

Observed behavior

compare-process-captures accepts imported capture Evidence containing an explicit unknown filesystem effect, then reports filesystem and overall unchanged with no first divergence when comparing that input with itself. Comparing it with the same observed final file whose creation is established instead reports filesystem changed and a found divergence. The input unknown is generated by REA’s production effect classifier, rather than hand-writing an unknown state.

Reproduction

Use Node 24.22.1 in a checkout at the revision below. A fresh checkout needs npm ci && npm run build:cached; verification rebuilt the runtime using already installed locked dependencies, and did not rerun installation. Set WORK to an empty disposable directory, then run this from the repository root. Each of the three comparisons below was executed once. The fixtures are synthetic imported Evidence; no target process or analysis provider runs.

export WORK="${WORK:?set WORK to an empty disposable directory}"
mkdir -p "$WORK"/{home,config,data,cache,state,tmp}
export HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" XDG_DATA_HOME="$WORK/data"
export XDG_CACHE_HOME="$WORK/cache" XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/tmp"
node --input-type=module <<'JS'
import { writeFileSync } from 'node:fs';
import { EMPTY_PROCESS_CAPTURE_EXAMPLE as example } from './dist/domain/process/processCaptureExample.js';
import { parseProcessCapture } from './dist/domain/process/processCaptureParsing.js';
import { classifyFilesystemEffects } from './dist/process/capture/ProcessFilesystemEffects.js';
import { createEvidence } from './dist/domain/evidence.js';
import { PROCESS_PROVIDER } from './dist/domain/process/processEvidenceProvider.js';
const file = {path: 'root_0:result.txt', type: 'file', mode: 0o100644,
  size: 4, sha256: 'a'.repeat(64), symlink_target: null};
for (const kind of ['unknown', 'complete']) {
  const capture = structuredClone(example);
  const before = capture.truncation_details.filesystem_before;
  const after = capture.truncation_details.filesystem_after;
  if (kind === 'complete') before.unobserved_subtrees = after.unobserved_subtrees = [];
  else { delete before.unobserved_subtrees; delete after.unobserved_subtrees; }
  after.hashed_bytes = file.size;
  const effects = classifyFilesystemEffects(
    {files: [], completeRoots: ['root_0'], coverage: before, truncated: false},
    {files: [file], completeRoots: ['root_0'], coverage: after, truncated: false});
  capture.files_after = [file];
  capture.filesystem_effects = effects;
  Object.assign(capture.filesystem_checkpoints[1], {files: [file], effects});
  const result = parseProcessCapture(capture);
  const evidence = createEvidence(undefined, PROCESS_PROVIDER, {
    predicateType: 'rea.process-capture', operation: 'capture_process_scenario',
    parameters: {}, result, confidence: 'observed', authority: 'controlled-replay',
    environment: {id: 'fixture', platform: 'fixture', architecture: 'fixture', isolation: 'process'},
    limitations: result.limitations});
  writeFileSync(`${process.env.WORK}/${kind}.json`, JSON.stringify(evidence));
  console.log(`${kind} effect=${effects[0].status}`);
}
JS
for pair in unknown:unknown unknown:complete complete:complete; do
  left="${pair%:*}"; right="${pair#*:}"
  node scripts/rea.mjs compare-process-captures "$WORK/$left.json" "$WORK/$right.json" --json > "$WORK/$left-$right.json"
done
node --input-type=module <<'JS'
import { readFileSync } from 'node:fs';
for (const pair of ['unknown-unknown', 'unknown-complete', 'complete-complete']) {
  const r = JSON.parse(readFileSync(`${process.env.WORK}/${pair}.json`, 'utf8')).normalized_result;
  console.log(`${pair}: status=${r.status} filesystem=${r.filesystem} first_divergence=${r.first_divergence.status}`);
}
JS

Expected behavior

The filesystem dimension must remain unknown when either input contains an unresolved filesystem effect. Neither of the first two comparisons can establish overall equivalence or locate a definite first divergence; the complete-input control should remain unchanged. docs/process-capture.md:251-272 defines null in an unknown effect as unobserved and says historical captures without subtree facts do not establish descendant absence. Lines 85-88 and 526-528 require uncertainty to block complete equivalence and affected dimensions to remain unknown; the compare_process_captures contract in src/contracts/sessionToolContracts.ts:102-103 also promises that missing observations are never treated as equivalent.

Impact and scope

P2: callers receive false certainty from an accepted imported capture whose per-effect uncertainty is not duplicated in residual_unknowns or truncation counters. Both original observations remain intact, so callers can inspect checkpoint effects and withhold conclusions whenever an effect is unknown. The executed fixtures omit optional subtree facts and have no filesystem residual. Current capture production also propagates unknown-effect reasons into residuals; a current live capture or historical saved file producing the demonstrated shape was not tested. Downstream reconstruction consequences and population frequency remain untested.

REA version

Exact node scripts/rea.mjs --version output: 6.8.0. Source commit: ec40c10749b5c4091fb08bc28c6f3f38c2a6968e.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; Darwin arm64. No Hopper, Ghidra, IDA, device, or external service used.

Target and artifact context

Two parser-validated process-capture Evidence JSON fixtures built from the public empty-capture example. Both retain the same hashed final file. Omitting optional subtree facts yields an unknown effect; supplying empty subtree-fact arrays establishes creation. The surrounding captures are synthetic and do not claim live execution provenance.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

All three CLI calls exited 0. The production classifier and returned normalized_result fields printed:

unknown effect=unknown
complete effect=created
unknown-unknown: status=unchanged filesystem=unchanged first_divergence=none
unknown-complete: status=changed filesystem=changed first_divergence=found
complete-complete: status=unchanged filesystem=unchanged first_divergence=none

The unknown effect retains before: null, the observed final file in after, and the reason The before snapshot has no subtree coverage facts; descendant absence remains unknown. Both comparison inputs pass the production capture parser and the CLI’s Evidence/workflow/provider admission. No temporary test or production source change is needed.

Analysis or suspected cause

Source-derived: src/domain/process/processComparison.ts:140-151,386-418 compares checkpoint JSON after checking normalization, residual scopes and source truncation. src/domain/process/processCaptureCoverage.ts:109-112,127-143 checks enumeration and hash coverage, without considering individual unknown effects or missing optional subtree facts. Identical unresolved effects therefore compare equal, while an unresolved effect versus established creation compares different. Current lifecycle residual propagation at src/process/capture/ProcessCaptureLifecycle.ts:425-429 explains why an ordinary newly produced capture can avoid this imported-record failure.

Additional context

Closed #1231 and merged #1274 established unknown absence effects in partial captures; closed #6444 addressed missing symlink subtree uncertainty during capture. Here the imported effect already says unknown, and the comparison loses that uncertainty. Open #7295 concerns a passing raw-only trace erasing rendered-terminal uncertainty; this reproduction supplies no trace. No security boundary crossing was observed. Last known good version: NOT_ENOUGH_INFO; no regression boundary was established.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7373 ## 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 Process capture or runtime observation ## Observed behavior `compare-process-captures` accepts imported capture Evidence containing an explicit unknown filesystem effect, then reports filesystem and overall `unchanged` with no first divergence when comparing that input with itself. Comparing it with the same observed final file whose creation is established instead reports filesystem `changed` and a found divergence. The input unknown is generated by REA’s production effect classifier, rather than hand-writing an unknown state. ## Reproduction Use Node 24.22.1 in a checkout at the revision below. A fresh checkout needs `npm ci && npm run build:cached`; verification rebuilt the runtime using already installed locked dependencies, and did not rerun installation. Set `WORK` to an empty disposable directory, then run this from the repository root. Each of the three comparisons below was executed once. The fixtures are synthetic imported Evidence; no target process or analysis provider runs. ```sh export WORK="${WORK:?set WORK to an empty disposable directory}" mkdir -p "$WORK"/{home,config,data,cache,state,tmp} export HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" XDG_DATA_HOME="$WORK/data" export XDG_CACHE_HOME="$WORK/cache" XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/tmp" node --input-type=module <<'JS' import { writeFileSync } from 'node:fs'; import { EMPTY_PROCESS_CAPTURE_EXAMPLE as example } from './dist/domain/process/processCaptureExample.js'; import { parseProcessCapture } from './dist/domain/process/processCaptureParsing.js'; import { classifyFilesystemEffects } from './dist/process/capture/ProcessFilesystemEffects.js'; import { createEvidence } from './dist/domain/evidence.js'; import { PROCESS_PROVIDER } from './dist/domain/process/processEvidenceProvider.js'; const file = {path: 'root_0:result.txt', type: 'file', mode: 0o100644, size: 4, sha256: 'a'.repeat(64), symlink_target: null}; for (const kind of ['unknown', 'complete']) { const capture = structuredClone(example); const before = capture.truncation_details.filesystem_before; const after = capture.truncation_details.filesystem_after; if (kind === 'complete') before.unobserved_subtrees = after.unobserved_subtrees = []; else { delete before.unobserved_subtrees; delete after.unobserved_subtrees; } after.hashed_bytes = file.size; const effects = classifyFilesystemEffects( {files: [], completeRoots: ['root_0'], coverage: before, truncated: false}, {files: [file], completeRoots: ['root_0'], coverage: after, truncated: false}); capture.files_after = [file]; capture.filesystem_effects = effects; Object.assign(capture.filesystem_checkpoints[1], {files: [file], effects}); const result = parseProcessCapture(capture); const evidence = createEvidence(undefined, PROCESS_PROVIDER, { predicateType: 'rea.process-capture', operation: 'capture_process_scenario', parameters: {}, result, confidence: 'observed', authority: 'controlled-replay', environment: {id: 'fixture', platform: 'fixture', architecture: 'fixture', isolation: 'process'}, limitations: result.limitations}); writeFileSync(`${process.env.WORK}/${kind}.json`, JSON.stringify(evidence)); console.log(`${kind} effect=${effects[0].status}`); } JS for pair in unknown:unknown unknown:complete complete:complete; do left="${pair%:*}"; right="${pair#*:}" node scripts/rea.mjs compare-process-captures "$WORK/$left.json" "$WORK/$right.json" --json > "$WORK/$left-$right.json" done node --input-type=module <<'JS' import { readFileSync } from 'node:fs'; for (const pair of ['unknown-unknown', 'unknown-complete', 'complete-complete']) { const r = JSON.parse(readFileSync(`${process.env.WORK}/${pair}.json`, 'utf8')).normalized_result; console.log(`${pair}: status=${r.status} filesystem=${r.filesystem} first_divergence=${r.first_divergence.status}`); } JS ``` ## Expected behavior The filesystem dimension must remain `unknown` when either input contains an unresolved filesystem effect. Neither of the first two comparisons can establish overall equivalence or locate a definite first divergence; the complete-input control should remain unchanged. `docs/process-capture.md:251-272` defines null in an unknown effect as unobserved and says historical captures without subtree facts do not establish descendant absence. Lines 85-88 and 526-528 require uncertainty to block complete equivalence and affected dimensions to remain unknown; the `compare_process_captures` contract in `src/contracts/sessionToolContracts.ts:102-103` also promises that missing observations are never treated as equivalent. ## Impact and scope P2: callers receive false certainty from an accepted imported capture whose per-effect uncertainty is not duplicated in `residual_unknowns` or truncation counters. Both original observations remain intact, so callers can inspect checkpoint effects and withhold conclusions whenever an effect is unknown. The executed fixtures omit optional subtree facts and have no filesystem residual. Current capture production also propagates unknown-effect reasons into residuals; a current live capture or historical saved file producing the demonstrated shape was not tested. Downstream reconstruction consequences and population frequency remain untested. ## REA version Exact `node scripts/rea.mjs --version` output: `6.8.0`. Source commit: `ec40c10749b5c4091fb08bc28c6f3f38c2a6968e`. ## Runtime and platform Node.js `v24.22.1`; macOS `27.0.1`; `Darwin arm64`. No Hopper, Ghidra, IDA, device, or external service used. ## Target and artifact context Two parser-validated process-capture Evidence JSON fixtures built from the public empty-capture example. Both retain the same hashed final file. Omitting optional subtree facts yields an unknown effect; supplying empty subtree-fact arrays establishes creation. The surrounding captures are synthetic and do not claim live execution provenance. ## Interface CLI ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence All three CLI calls exited 0. The production classifier and returned `normalized_result` fields printed: ```text unknown effect=unknown complete effect=created unknown-unknown: status=unchanged filesystem=unchanged first_divergence=none unknown-complete: status=changed filesystem=changed first_divergence=found complete-complete: status=unchanged filesystem=unchanged first_divergence=none ``` The unknown effect retains `before: null`, the observed final file in `after`, and the reason `The before snapshot has no subtree coverage facts; descendant absence remains unknown.` Both comparison inputs pass the production capture parser and the CLI’s Evidence/workflow/provider admission. No temporary test or production source change is needed. ## Analysis or suspected cause Source-derived: `src/domain/process/processComparison.ts:140-151,386-418` compares checkpoint JSON after checking normalization, residual scopes and source truncation. `src/domain/process/processCaptureCoverage.ts:109-112,127-143` checks enumeration and hash coverage, without considering individual unknown effects or missing optional subtree facts. Identical unresolved effects therefore compare equal, while an unresolved effect versus established creation compares different. Current lifecycle residual propagation at `src/process/capture/ProcessCaptureLifecycle.ts:425-429` explains why an ordinary newly produced capture can avoid this imported-record failure. ## Additional context Closed [#1231](https://github.com/morluto/rea/issues/1231) and merged [#1274](https://github.com/morluto/rea/pull/1274) established unknown absence effects in partial captures; closed [#6444](https://github.com/morluto/rea/issues/6444) addressed missing symlink subtree uncertainty during capture. Here the imported effect already says unknown, and the comparison loses that uncertainty. Open [#7295](https://github.com/morluto/rea/issues/7295) concerns a passing raw-only trace erasing rendered-terminal uncertainty; this reproduction supplies no trace. No security boundary crossing was observed. Last known good version: NOT_ENOUGH_INFO; no regression boundary was established.
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#4381
No description provided.