[GH-ISSUE #6444] [Bug] capture-process omits filesystem uncertainty for a symlink observation root #3858

Closed
opened 2026-10-31 00:44:41 +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/6444

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

capture-process selects a stable symlink to a directory. The target creates link/final.txt, but the returned capture contains only the unchanged link, reports complete filesystem enumeration, and includes no filesystem residual unknown explaining the unobserved subtree. This reproduced in 2/2 fresh creation trials. Selecting the actual directory instead recorded the created file in the control.

The link itself is correctly classified as unchanged. The defect is the missing coverage limitation for its descendants.

Reproduction

From a fresh checkout at 9eb829aca34be36ff112826337996f6139f82eff, prepare the locked dependencies and runtime:

npm ci && npm run build:cached

The observed trials used an already built checkout at that revision. From the checkout root, this inline fixture runs two fresh symlink cases and a resolved-directory control:

python3 - <<'PYTHON'
import json, os, pathlib, subprocess, tempfile

rea = pathlib.Path.cwd() / 'scripts/rea.mjs'
work = pathlib.Path(tempfile.mkdtemp())
(work / 'home').mkdir()
env = {**os.environ, 'HOME': str(work / 'home')}
for name, selected in [('link-1', './link'), ('link-2', './link'),
                       ('control', './real/reports')]:
    run = work / name
    (run / 'real/reports').mkdir(parents=True)
    (run / 'link').symlink_to('real/reports')
    scenario = {
        'executable': 'node',
        'arguments': ['-e',
            "require('node:fs').writeFileSync('link/final.txt','done')"],
        'working_directory': str(run),
        'filesystem_observation_paths': [selected],
        'timeout_ms': 5000, 'settle_ms': 400,
    }
    config = run / 'scenario.json'
    config.write_text(json.dumps(scenario))
    result = subprocess.run(
        ['node', str(rea), 'capture-process', str(config), '--format', 'json'],
        env=env, capture_output=True, text=True, check=True)
    capture = json.loads(result.stdout)['normalized_result']
    print(name, {
        'disk_contents': (run / 'real/reports/final.txt').read_text(),
        'effects': [(e['path'], e['status'])
                    for e in capture['filesystem_effects']],
        'enumeration_truncated': [
            capture['truncation_details'][phase]['enumeration_truncated']
            for phase in ['filesystem_before', 'filesystem_after']],
        'filesystem_unknowns': [u for u in capture['residual_unknowns']
                               if u['scope'] == 'filesystem'],
    })
PYTHON

The fixture uses the public CLI without mocks, hooks, concurrent writers, or external providers.

Expected behavior

The filesystem capture contract says descendants of unfollowed symlinks remain unknown. The result should retain the observed link metadata and explicitly identify the unobserved subtree as filesystem uncertainty. Complete link metadata should not conceal incomplete descendant coverage.

Following the symlink is unnecessary; retaining the existing no-follow behavior with a meaningful filesystem residual unknown would expose the limitation.

Impact and scope

P2: a caller selecting a symlinked directory receives no machine-readable filesystem uncertainty for persistent writes below that selection. The capture remains usable for other observations. A practical workaround for a stable fixture is to select the resolved directory; the executed control recorded root_0:final.txt as created with a digest.

No target data loss or security-boundary crossing was observed. Other operating systems, junctions, and production frequency were not tested.

REA version

6.7.0 (source 9eb829aca3)

Runtime and platform

Node.js v26.12.1; macOS 27.0.1; arm64. Native PTY process capture; no deep-analysis provider.

Target and artifact context

A local Node.js child writes a four-byte file through a relative symlink to a directory. The selected artifact is process capture Evidence, including filesystem checkpoints and residual unknowns.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

Both creation trials returned CLI exit 0 and target exit code 0. Direct filesystem reads returned done. The common projection of normalized_result was:

files_before: [root_0:. (symlink -> real/reports)]
files_after:  [root_0:. (symlink -> real/reports)]
filesystem_effects: [(root_0:., unchanged)]
filesystem_before.enumeration_truncated: false
filesystem_after.enumeration_truncated: false
both enumeration_reasons: []
both enumeration_omissions: []
residual_unknowns filtered to scope=filesystem: []

The filesystem coverage fields above are under truncation_details. The control included (root_0:final.txt, created) with complete enumeration. Global truncated was true due to process sampling in these runs; there was no filesystem truncation or filesystem unknown.

Typed outcome or recovery expected

Return successful capture Evidence with an explicit filesystem residual unknown identifying the unfollowed symlink subtree. Preserve the correct unchanged status of the link itself. The resolved-directory control should continue to report the created file.

Analysis or suspected cause

Source-derived explanation: the symlink snapshot branch returns true after recording only link metadata. The root loop therefore marks the root complete, and coverage construction reports untruncated enumeration.

Effect classification visits only paths observed in at least one checkpoint. Its symlink-descendant guards cannot emit an unknown for a descendant missing from both snapshots. Residual collection derives filesystem uncertainty from unknown effects and coverage omissions, neither of which identifies this subtree.

Additional context

Related issues checked include #1231 (false deletion under budget truncation), #3303 (changed literal symlink targets), and #4575 (symlink disappearance during observation). This fixture has a stable, readable link and no enumeration race. No matching open or closed report was found.

Last-known-good version: NOT_ENOUGH_INFO; no historical passing revision was tested. No downstream false-equivalence claim is made.

Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6444 ### 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 `capture-process` selects a stable symlink to a directory. The target creates `link/final.txt`, but the returned capture contains only the unchanged link, reports complete filesystem enumeration, and includes no filesystem residual unknown explaining the unobserved subtree. This reproduced in 2/2 fresh creation trials. Selecting the actual directory instead recorded the created file in the control. The link itself is correctly classified as unchanged. The defect is the missing coverage limitation for its descendants. ### Reproduction From a fresh checkout at `9eb829aca34be36ff112826337996f6139f82eff`, prepare the locked dependencies and runtime: ```sh npm ci && npm run build:cached ``` The observed trials used an already built checkout at that revision. From the checkout root, this inline fixture runs two fresh symlink cases and a resolved-directory control: ```sh python3 - <<'PYTHON' import json, os, pathlib, subprocess, tempfile rea = pathlib.Path.cwd() / 'scripts/rea.mjs' work = pathlib.Path(tempfile.mkdtemp()) (work / 'home').mkdir() env = {**os.environ, 'HOME': str(work / 'home')} for name, selected in [('link-1', './link'), ('link-2', './link'), ('control', './real/reports')]: run = work / name (run / 'real/reports').mkdir(parents=True) (run / 'link').symlink_to('real/reports') scenario = { 'executable': 'node', 'arguments': ['-e', "require('node:fs').writeFileSync('link/final.txt','done')"], 'working_directory': str(run), 'filesystem_observation_paths': [selected], 'timeout_ms': 5000, 'settle_ms': 400, } config = run / 'scenario.json' config.write_text(json.dumps(scenario)) result = subprocess.run( ['node', str(rea), 'capture-process', str(config), '--format', 'json'], env=env, capture_output=True, text=True, check=True) capture = json.loads(result.stdout)['normalized_result'] print(name, { 'disk_contents': (run / 'real/reports/final.txt').read_text(), 'effects': [(e['path'], e['status']) for e in capture['filesystem_effects']], 'enumeration_truncated': [ capture['truncation_details'][phase]['enumeration_truncated'] for phase in ['filesystem_before', 'filesystem_after']], 'filesystem_unknowns': [u for u in capture['residual_unknowns'] if u['scope'] == 'filesystem'], }) PYTHON ``` The fixture uses the public CLI without mocks, hooks, concurrent writers, or external providers. ### Expected behavior The [filesystem capture contract](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/process-capture.md#L240) says descendants of unfollowed symlinks remain unknown. The result should retain the observed link metadata and explicitly identify the unobserved subtree as filesystem uncertainty. Complete link metadata should not conceal incomplete descendant coverage. Following the symlink is unnecessary; retaining the existing no-follow behavior with a meaningful filesystem residual unknown would expose the limitation. ### Impact and scope P2: a caller selecting a symlinked directory receives no machine-readable filesystem uncertainty for persistent writes below that selection. The capture remains usable for other observations. A practical workaround for a stable fixture is to select the resolved directory; the executed control recorded `root_0:final.txt` as created with a digest. No target data loss or security-boundary crossing was observed. Other operating systems, junctions, and production frequency were not tested. ### REA version 6.7.0 (source 9eb829aca34be36ff112826337996f6139f82eff) ### Runtime and platform Node.js v26.12.1; macOS 27.0.1; arm64. Native PTY process capture; no deep-analysis provider. ### Target and artifact context A local Node.js child writes a four-byte file through a relative symlink to a directory. The selected artifact is process capture Evidence, including filesystem checkpoints and residual unknowns. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence Both creation trials returned CLI exit 0 and target exit code 0. Direct filesystem reads returned `done`. The common projection of `normalized_result` was: ```text files_before: [root_0:. (symlink -> real/reports)] files_after: [root_0:. (symlink -> real/reports)] filesystem_effects: [(root_0:., unchanged)] filesystem_before.enumeration_truncated: false filesystem_after.enumeration_truncated: false both enumeration_reasons: [] both enumeration_omissions: [] residual_unknowns filtered to scope=filesystem: [] ``` The filesystem coverage fields above are under `truncation_details`. The control included `(root_0:final.txt, created)` with complete enumeration. Global `truncated` was true due to process sampling in these runs; there was no filesystem truncation or filesystem unknown. ### Typed outcome or recovery expected Return successful capture Evidence with an explicit filesystem residual unknown identifying the unfollowed symlink subtree. Preserve the correct unchanged status of the link itself. The resolved-directory control should continue to report the created file. ### Analysis or suspected cause Source-derived explanation: [the symlink snapshot branch](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/process/capture/FilesystemSnapshot.ts#L447) returns true after recording only link metadata. [The root loop](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/process/capture/FilesystemSnapshot.ts#L635) therefore marks the root complete, and [coverage construction](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/process/capture/FilesystemSnapshot.ts#L607) reports untruncated enumeration. [Effect classification](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/process/capture/ProcessFilesystemEffects.ts#L37) visits only paths observed in at least one checkpoint. Its symlink-descendant guards cannot emit an unknown for a descendant missing from both snapshots. [Residual collection](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/process/capture/ProcessCaptureLifecycle.ts#L320) derives filesystem uncertainty from unknown effects and [coverage omissions](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/process/capture/ProcessCaptureLifecycle.ts#L445), neither of which identifies this subtree. ### Additional context Related issues checked include [#1231](https://github.com/morluto/rea/issues/1231) (false deletion under budget truncation), [#3303](https://github.com/morluto/rea/issues/3303) (changed literal symlink targets), and [#4575](https://github.com/morluto/rea/issues/4575) (symlink disappearance during observation). This fixture has a stable, readable link and no enumeration race. No matching open or closed report was found. Last-known-good version: NOT_ENOUGH_INFO; no historical passing revision was tested. No downstream false-equivalence claim is made.
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#3858
No description provided.