[GH-ISSUE #7584] [Bug] Path normalization skips descendants when an observation root ends in a slash #4519

Closed
opened 2026-10-31 00:50:28 +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/7584

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

node scripts/rea.mjs capture-process scenario.json --json accepts an existing observation directory ending in /, runs the selected command successfully, and leaves that directory's absolute descendant path literal despite normalization.paths: true. Selecting the same directory without the slash produces the filesystem-root alias. A /. suffix also leaves the descendant literal.

The executed fixture below invokes /bin/echo with the same descendant argument in all three cases. Normalized frame data and the first rendered line disagree with the plain-directory control solely because the selected root's spelling changes. Every capture and target exited 0.

Reproduction

  1. Use the built baseline below with its locked dependencies and Node.js v24.22.1 on a supported macOS capture host.
  2. Run this inline fixture from the checkout root. It creates an ordinary directory outside the working directory, isolates HOME/XDG paths, and removes its temporary tree afterward:
fixture=$(mktemp -d)
node --input-type=module - "$fixture" <<'JS'
import fs from 'node:fs';
import { spawnSync } from 'node:child_process';
const root = fs.realpathSync(process.argv[2]);
const env = { ...process.env };
for (const [key, folder] of Object.entries({
  HOME: 'home', XDG_CONFIG_HOME: 'config', XDG_CACHE_HOME: 'cache',
  XDG_DATA_HOME: 'data', XDG_STATE_HOME: 'state',
})) {
  fs.mkdirSync(`${root}/${folder}`);
  env[key] = `${root}/${folder}`;
}
fs.mkdirSync(`${root}/work`);
fs.mkdirSync(`${root}/selected`);
fs.writeFileSync(`${root}/selected/item.txt`, 'fixture\n');
try {
  for (const [label, suffix] of [['plain', ''], ['slash', '/'], ['dot', '/.']]) {
    const scenario = {
      executable: '/bin/echo',
      arguments: [`P=${root}/selected/item.txt`],
      working_directory: `${root}/work`,
      filesystem_observation_paths: [`${root}/selected${suffix}`],
      terminal: { columns: 512, rows: 4, scrollback: 0 },
      normalization: { paths: true, pids: false, ports: false },
    };
    const file = `${root}/${label}.json`;
    fs.writeFileSync(file, JSON.stringify(scenario));
    const run = spawnSync(process.execPath,
      ['scripts/rea.mjs', 'capture-process', file, '--json'],
      { env, encoding: 'utf8', timeout: 30000, maxBuffer: 4000000 });
    if (run.error) throw run.error;
    if (run.status !== 0) throw new Error(run.stderr || run.stdout);
    const r = JSON.parse(run.stdout).normalized_result;
    console.log(JSON.stringify({ label, cli_exit: run.status, target_exit: r.exit,
      data: r.frames.map(f => f.data).join(''),
      first_line: r.rendered_frames.at(-1).lines[0],
      path_rule: r.manifest.comparison_contract.path_normalization_version,
    }).replaceAll(root, '<fixture>'));
  }
} finally {
  fs.rmSync(root, { recursive: true, force: true });
}
JS
  1. Compare data and first_line. The plain case contains <filesystem-root-0>/item.txt; slash and dot cases contain <fixture>/selected/item.txt. <fixture> is substituted by the reporting script for its generated absolute fixture directory.

The exact inline fixture ran once, with both failing spellings reproduced. Separate CLI fixtures reproduced slash and slash-dot descendant failures twice each, with plain-root controls succeeding.

Expected behavior

With path normalization enabled, a selected parent directory should normalize within a descendant path, as promised by the process-capture guide. Requiring the selected directory through a trailing / or /. should preserve that descendant-normalization behavior. All three cases should produce P=<filesystem-root-0>/item.txt in normalized frame data and the unwrapped rendered line.

Impact and scope

P2: valid captures retain machine-specific descendant text in the comparison projection for these observation-root spellings. The observed mismatch affects normalized frame data, rendered lines, and serialized terminal text. Selecting the same existing directory without / or /. is a demonstrated workaround. Raw PTY text remains available; this report does not claim target mutation or raw-evidence loss.

Only local macOS CLI captures were exercised. No full comparison, trace-specification assertion, MCP capture, Linux run or installed-release run was executed. Production frequency and the historical first affected revision are unknown.

REA version

Exact node scripts/rea.mjs --version output:

6.8.0

Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; Darwin arm64; locked @lydell/node-pty package 1.1.0. The public CLI performed real native PTY captures.

Target and artifact context

Local /bin/echo, one ordinary observation directory containing item.txt, and a separate working directory. No analysis provider or external target was involved.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

Reduced output from the exact inline fixture; CR/LF is shown escaped:

plain: cli_exit=0, target_exit=0, data="P=<filesystem-root-0>/item.txt\r\n"
slash: cli_exit=0, target_exit=0, data="P=<fixture>/selected/item.txt\r\n"
dot:   cli_exit=0, target_exit=0, data="P=<fixture>/selected/item.txt\r\n"

first_line contains the corresponding text without CR/LF in each case. Each capture reports path_rule: "path-component-boundaries-v2". Separate wide-terminal captures also retained the literal descendant in rendered_frames[].serialized_state. Their comparison contracts retain the trailing slash, including the runtime conversion of /. to /.

Baseline contract and source checks:

  • docs/process-capture.md:329–334 promises parent-directory normalization within descendant paths under path-component-boundaries-v2.
  • src/filesystem/callerPath.ts:19–24 preserves trailing / or /. as a directory requirement. src/process/capture/ProcessScenarioRuntimeValidation.ts:64–71 passes observation paths through that conversion.
  • src/process/capture/ProcessNormalization.ts:347–390 builds the selected-root aliases and path-boundary matcher.
  • Existing descendant/most-specific-path and sibling-name controls are in src/process/capture/ProcessNormalization.test.ts:487–552. The complete existing file passed all 23 tests during verification.

Typed outcome or recovery expected

Return successful capture Evidence with the selected directory alias applied to descendant text. No new error category is required for this accepted directory selection.

Analysis or suspected cause

Source-derived explanation: the root alias key retains its final separator. After matching the full key, the regular expression still requires an end, another slash, whitespace or a quote. The first character of item.txt satisfies none of those conditions, so the selected-root match is skipped. This agrees with the observed plain/slash controls; no fix was implemented or verified.

Additional context

The duplicate search included open and closed issues, all-state pull requests, issue comments and the closest PR's discussion. No matching trailing-separator descendant-normalization report was found. Some broad searches reached their result cap, and two later comment searches were rate-limited; earlier comment searching and direct relevant discussion reads succeeded.

Related issue #6586 fixes accidental aliasing of sibling executable names. PR #2468 fixes parent-first replacement of nested identities. Neither describes this slash-terminated root failing to match a descendant. Issue #6446 concerns permission-denial classification; issue #7281 concerns removed observer scratch. Their triggers are absent here.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Real successful CLI captures leave selected descendant text literal in normalized and rendered observations when the existing observation directory ends in a separator.
  • Workaround status: available
  • Workaround basis: The executed plain-directory control returns the expected alias for the same existing directory and descendant while retaining path normalization.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7584 ### 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 `node scripts/rea.mjs capture-process scenario.json --json` accepts an existing observation directory ending in `/`, runs the selected command successfully, and leaves that directory's absolute descendant path literal despite `normalization.paths: true`. Selecting the same directory without the slash produces the filesystem-root alias. A `/.` suffix also leaves the descendant literal. The executed fixture below invokes `/bin/echo` with the same descendant argument in all three cases. Normalized frame data and the first rendered line disagree with the plain-directory control solely because the selected root's spelling changes. Every capture and target exited 0. ### Reproduction 1. Use the built baseline below with its locked dependencies and Node.js v24.22.1 on a supported macOS capture host. 2. Run this inline fixture from the checkout root. It creates an ordinary directory outside the working directory, isolates HOME/XDG paths, and removes its temporary tree afterward: ```sh fixture=$(mktemp -d) node --input-type=module - "$fixture" <<'JS' import fs from 'node:fs'; import { spawnSync } from 'node:child_process'; const root = fs.realpathSync(process.argv[2]); const env = { ...process.env }; for (const [key, folder] of Object.entries({ HOME: 'home', XDG_CONFIG_HOME: 'config', XDG_CACHE_HOME: 'cache', XDG_DATA_HOME: 'data', XDG_STATE_HOME: 'state', })) { fs.mkdirSync(`${root}/${folder}`); env[key] = `${root}/${folder}`; } fs.mkdirSync(`${root}/work`); fs.mkdirSync(`${root}/selected`); fs.writeFileSync(`${root}/selected/item.txt`, 'fixture\n'); try { for (const [label, suffix] of [['plain', ''], ['slash', '/'], ['dot', '/.']]) { const scenario = { executable: '/bin/echo', arguments: [`P=${root}/selected/item.txt`], working_directory: `${root}/work`, filesystem_observation_paths: [`${root}/selected${suffix}`], terminal: { columns: 512, rows: 4, scrollback: 0 }, normalization: { paths: true, pids: false, ports: false }, }; const file = `${root}/${label}.json`; fs.writeFileSync(file, JSON.stringify(scenario)); const run = spawnSync(process.execPath, ['scripts/rea.mjs', 'capture-process', file, '--json'], { env, encoding: 'utf8', timeout: 30000, maxBuffer: 4000000 }); if (run.error) throw run.error; if (run.status !== 0) throw new Error(run.stderr || run.stdout); const r = JSON.parse(run.stdout).normalized_result; console.log(JSON.stringify({ label, cli_exit: run.status, target_exit: r.exit, data: r.frames.map(f => f.data).join(''), first_line: r.rendered_frames.at(-1).lines[0], path_rule: r.manifest.comparison_contract.path_normalization_version, }).replaceAll(root, '<fixture>')); } } finally { fs.rmSync(root, { recursive: true, force: true }); } JS ``` 3. Compare `data` and `first_line`. The plain case contains `<filesystem-root-0>/item.txt`; slash and dot cases contain `<fixture>/selected/item.txt`. `<fixture>` is substituted by the reporting script for its generated absolute fixture directory. The exact inline fixture ran once, with both failing spellings reproduced. Separate CLI fixtures reproduced slash and slash-dot descendant failures twice each, with plain-root controls succeeding. ### Expected behavior With path normalization enabled, a selected parent directory should normalize within a descendant path, as promised by the process-capture guide. Requiring the selected directory through a trailing `/` or `/.` should preserve that descendant-normalization behavior. All three cases should produce `P=<filesystem-root-0>/item.txt` in normalized frame data and the unwrapped rendered line. ### Impact and scope P2: valid captures retain machine-specific descendant text in the comparison projection for these observation-root spellings. The observed mismatch affects normalized frame data, rendered lines, and serialized terminal text. Selecting the same existing directory without `/` or `/.` is a demonstrated workaround. Raw PTY text remains available; this report does not claim target mutation or raw-evidence loss. Only local macOS CLI captures were exercised. No full comparison, trace-specification assertion, MCP capture, Linux run or installed-release run was executed. Production frequency and the historical first affected revision are unknown. ### REA version Exact `node scripts/rea.mjs --version` output: ```text 6.8.0 ``` Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js `v24.22.1`; Darwin arm64; locked `@lydell/node-pty` package `1.1.0`. The public CLI performed real native PTY captures. ### Target and artifact context Local `/bin/echo`, one ordinary observation directory containing `item.txt`, and a separate working directory. No analysis provider or external target was involved. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence Reduced output from the exact inline fixture; CR/LF is shown escaped: ```text plain: cli_exit=0, target_exit=0, data="P=<filesystem-root-0>/item.txt\r\n" slash: cli_exit=0, target_exit=0, data="P=<fixture>/selected/item.txt\r\n" dot: cli_exit=0, target_exit=0, data="P=<fixture>/selected/item.txt\r\n" ``` `first_line` contains the corresponding text without CR/LF in each case. Each capture reports `path_rule: "path-component-boundaries-v2"`. Separate wide-terminal captures also retained the literal descendant in `rendered_frames[].serialized_state`. Their comparison contracts retain the trailing slash, including the runtime conversion of `/.` to `/`. Baseline contract and source checks: - `docs/process-capture.md:329–334` promises parent-directory normalization within descendant paths under `path-component-boundaries-v2`. - `src/filesystem/callerPath.ts:19–24` preserves trailing `/` or `/.` as a directory requirement. `src/process/capture/ProcessScenarioRuntimeValidation.ts:64–71` passes observation paths through that conversion. - `src/process/capture/ProcessNormalization.ts:347–390` builds the selected-root aliases and path-boundary matcher. - Existing descendant/most-specific-path and sibling-name controls are in `src/process/capture/ProcessNormalization.test.ts:487–552`. The complete existing file passed all 23 tests during verification. ### Typed outcome or recovery expected Return successful capture Evidence with the selected directory alias applied to descendant text. No new error category is required for this accepted directory selection. ### Analysis or suspected cause Source-derived explanation: the root alias key retains its final separator. After matching the full key, the regular expression still requires an end, another slash, whitespace or a quote. The first character of `item.txt` satisfies none of those conditions, so the selected-root match is skipped. This agrees with the observed plain/slash controls; no fix was implemented or verified. ### Additional context The duplicate search included open and closed issues, all-state pull requests, issue comments and the closest PR's discussion. No matching trailing-separator descendant-normalization report was found. Some broad searches reached their result cap, and two later comment searches were rate-limited; earlier comment searching and direct relevant discussion reads succeeded. Related [issue #6586](https://github.com/morluto/rea/issues/6586) fixes accidental aliasing of sibling executable names. [PR #2468](https://github.com/morluto/rea/pull/2468) fixes parent-first replacement of nested identities. Neither describes this slash-terminated root failing to match a descendant. [Issue #6446](https://github.com/morluto/rea/issues/6446) concerns permission-denial classification; [issue #7281](https://github.com/morluto/rea/issues/7281) concerns removed observer scratch. Their triggers are absent here. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Real successful CLI captures leave selected descendant text literal in normalized and rendered observations when the existing observation directory ends in a separator. - Workaround status: available - Workaround basis: The executed plain-directory control returns the expected alias for the same existing directory and descendant while retaining path normalization.
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#4519
No description provided.