[GH-ISSUE #7625] [Bug] Rendered Markdown section bodies drop a filename's trailing space #4553

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

Before submitting

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

Area

CLI

Observed behavior

node scripts/rea.mjs inspect-keyed-archive "$WORK/report.plist " --format md exits successfully and includes the filename's final ASCII space in its raw archive-path paragraph. Rendering that Markdown with the checkout's locked VitePress renderer removes the final space. The paired --format json command retains it.

Two actual fixture files named "report.plist " and "report.plist" therefore produce the same rendered archive-path text. A leading-space filename remains distinguishable. These observations concern paragraph text under a section heading.

Reproduction

  1. Use the stated baseline checkout with its existing built runtime and locked dependencies, with Node 24.22.1 available as node. A fresh checkout requires npm ci && npm run build:cached first; this verification used the supplied already-built checkout.
  2. Run the following from the repository root. It builds the three binary plist fixtures, isolates the CLI's home/XDG/temp state, renders the actual command output, and deletes the generated files afterward.
export WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
python3 - "$WORK" <<'PY'
import pathlib, plistlib, sys
root = pathlib.Path(sys.argv[1])
archive = {'$version': 100000, '$archiver': 'NSKeyedArchiver',
           '$top': {'root': plistlib.UID(1)},
           '$objects': ['$null', {'$class': plistlib.UID(2), 'note': 'x'},
                        {'$classname': 'Fixture', '$classes': ['Fixture']}]}
for name in ['report.plist ', 'report.plist', ' leading.plist']:
    (root / name).write_bytes(plistlib.dumps(archive, fmt=plistlib.FMT_BINARY))
PY
node --input-type=module <<'JS'
import { mkdirSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { createMarkdownRenderer, disposeMdItInstance } from 'vitepress';
import { parseFragment } from 'parse5';
const root = process.env.WORK;
const env = { PATH: process.env.PATH, REA_LOG_LEVEL: 'silent' };
for (const [key, dir] of Object.entries({ HOME: 'home', XDG_CONFIG_HOME: 'config',
  XDG_CACHE_HOME: 'cache', XDG_DATA_HOME: 'data', XDG_STATE_HOME: 'state', TMPDIR: 'tmp' })) {
  env[key] = `${root}/${dir}`;
  mkdirSync(env[key], { recursive: true });
}
const run = args => {
  const r = spawnSync(process.execPath, ['scripts/rea.mjs', ...args],
    { env, encoding: 'utf8', timeout: 30000 });
  if (r.status !== 0 || r.stderr) throw new Error(JSON.stringify(r));
  return r.stdout;
};
console.log('version:', JSON.stringify(run(['--version'])));
const renderer = await createMarkdownRenderer(root, { linkify: false });
const text = n => n.nodeName === '#text' ? n.value : (n.childNodes ?? []).map(text).join('');
const paragraph = html => {
  const nodes = parseFragment(html).childNodes;
  const i = nodes.findIndex(n => n.tagName === 'h2' &&
    (n.childNodes ?? []).filter(c => c.nodeName === '#text').map(text).join('').trim()
      === 'normalized_result.archive_path');
  if (i < 0) throw new Error('Archive-path heading missing');
  const p = nodes.slice(i + 1).find(n => n.tagName === 'p');
  if (!p) throw new Error('Archive-path paragraph missing');
  return text(p);
};
for (const name of ['report.plist ', 'report.plist', ' leading.plist']) {
  const base = ['inspect-keyed-archive', `${root}/${name}`];
  const json = JSON.parse(run([...base, '--format', 'json']));
  const md = run([...base, '--format', 'md']);
  const heading = '## normalized_result.archive_path\n\n';
  const raw = md.split(heading)[1].split('\n\n## ')[0];
  console.log(JSON.stringify({ name, json: json.normalized_result.archive_path,
    raw, rendered: paragraph(renderer.render(md)) }));
  if (name.endsWith(' ')) {
    const encoded = md.replace(heading + raw,
      heading + raw.replace(/ +$/g, s => '&#32;'.repeat(s.length)));
    console.log('encoded-space control:', JSON.stringify(paragraph(renderer.render(encoded))));
  }
}
disposeMdItInstance();
JS

The trailing-space failure reproduced in the initial complete three-fixture comparison and again in this reduced replay. Each complete comparison ran six CLI commands, all with exit 0 and empty stderr.

Expected behavior

The rendered archive-path paragraph should retain the filename's final ASCII space, so it identifies the same file as the JSON result and remains distinguishable from the separate filename without that space. The existing contract treats heading and paragraph facts as literal data and requires preservation of caller-selected paths. Contract locators appear in the evidence section below.

Impact and scope

P2: a successful rendered report collapses two distinct input filenames to identical archive-path text. This is a bounded output-fidelity defect; the paired JSON command preserves the exact name and supplies a practical workaround. The input files and raw Markdown retain their source data. No stored Evidence loss was established.

The demonstrated scope is a scalar archive-path section body rendered with locked VitePress. The leading-space and plain-name controls work. Other whitespace, heading values, browser layout, other renderers and other platforms were not tested. TOON was not tested.

REA version

Exact node scripts/rea.mjs --version stdout: 6.8.0 followed by one newline.
Source commit: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1, arm64; checkout-locked VitePress 1.6.4 and parse5 8.0.1. No external analysis engine or GUI was used.

Target and artifact context

Three identical 203-byte binary NSKeyedArchiver plists, generated inline using Python plistlib, with filenames "report.plist ", "report.plist", and " leading.plist". The first filename ends in exactly one ASCII space.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

The independently executed reduced reproduction printed:

version: "6.8.0\n"
{"name":"report.plist ","json":"report.plist ","raw":"report.plist ","rendered":"report.plist"}
encoded-space control: "report.plist "
{"name":"report.plist","json":"report.plist","raw":"report.plist","rendered":"report.plist"}
{"name":" leading.plist","json":" leading.plist","raw":"&#32;leading.plist","rendered":" leading.plist"}

The observer reads paragraph text from parsed HTML, excluding VitePress's added heading permalink anchor. The encoded-space control changes only the in-memory emitted paragraph before rendering; no source fix was applied.

Applicable contract and failure locators at the stated baseline:

  • AGENTS.md:13 requires preservation of caller-selected inputs and local evidence, including paths and metadata.
  • src/cli/cliOutputFormatter.ts:69-77 declares heading/paragraph facts literal data and encodes leading whitespace, but has no corresponding trailing-space encoding.
  • src/cli/cliOutputFormatter.ts:153-155 and 178-181 project scalar facts as escaped section-body text.
  • tests/boundary/cli/markdownLiteralCells.test.ts:107-158 checks literal scalar, array and heading facts through the public CLI and locked renderer. Its examples do not cover trailing paragraph spaces.
  • docs/cli.md:965-973 explicitly promises literal text for table headers/cells. That paragraph is table-specific; the paragraph contract above comes from the owning formatter, scalar test and repository invariant.

Evidence provenance: independently observed public-CLI output and locked-renderer comparison; source locators explain the applicable contract and producing representation separately.

Analysis or suspected cause

Source-derived explanation: paragraph escaping protects initial whitespace but leaves a final ASCII space at the end of the Markdown source line. The tested renderer consumes that character as Markdown layout. Encoding that one space as a numeric character reference restores the expected paragraph text in the executed control. This points to paragraph representation rather than the archive parser, since the paired JSON output retains the filename.

Additional context

Closed issue #7432 requests exact edge spaces in tensor-name table cells, and merged PR #7506 explicitly implements a table-local encoder while keeping non-table escaping separate. Its full diff changes table headers/cells only. Neither states an all-position edge-space fix. This candidate concerns a distinct section-body position; no regression of that table fix is claimed. The merged revision was inspected, not executed as a second runtime baseline.

Open issue #7325 concerns brace-delimited provider JSON disappearing in table cells. Issue #7593 concerns NUL replacement. Their restoration checks differ. The duplicate search also covered open/closed issues, open/merged PRs and comments for paragraph, section-body, trailing-space and Markdown fidelity terms; no existing report of this section-body trailing-space failure was found.

The first affected release and last known good version are unknown. A proposed restoration check is equality of the rendered archive-path paragraph and JSON filename for all three fixtures, keeping their distinct names distinguishable.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Two actual files with different names produce identical rendered archive-path paragraph text despite successful public CLI completion.
  • Workaround status: available
  • Workaround basis: The paired JSON commands retain both exact filenames, including the trailing ASCII space.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7625 ### 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 CLI ### Observed behavior `node scripts/rea.mjs inspect-keyed-archive "$WORK/report.plist " --format md` exits successfully and includes the filename's final ASCII space in its raw archive-path paragraph. Rendering that Markdown with the checkout's locked VitePress renderer removes the final space. The paired `--format json` command retains it. Two actual fixture files named `"report.plist "` and `"report.plist"` therefore produce the same rendered archive-path text. A leading-space filename remains distinguishable. These observations concern paragraph text under a section heading. ### Reproduction 1. Use the stated baseline checkout with its existing built runtime and locked dependencies, with Node 24.22.1 available as `node`. A fresh checkout requires `npm ci && npm run build:cached` first; this verification used the supplied already-built checkout. 2. Run the following from the repository root. It builds the three binary plist fixtures, isolates the CLI's home/XDG/temp state, renders the actual command output, and deletes the generated files afterward. ```sh export WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT python3 - "$WORK" <<'PY' import pathlib, plistlib, sys root = pathlib.Path(sys.argv[1]) archive = {'$version': 100000, '$archiver': 'NSKeyedArchiver', '$top': {'root': plistlib.UID(1)}, '$objects': ['$null', {'$class': plistlib.UID(2), 'note': 'x'}, {'$classname': 'Fixture', '$classes': ['Fixture']}]} for name in ['report.plist ', 'report.plist', ' leading.plist']: (root / name).write_bytes(plistlib.dumps(archive, fmt=plistlib.FMT_BINARY)) PY node --input-type=module <<'JS' import { mkdirSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import { createMarkdownRenderer, disposeMdItInstance } from 'vitepress'; import { parseFragment } from 'parse5'; const root = process.env.WORK; const env = { PATH: process.env.PATH, REA_LOG_LEVEL: 'silent' }; for (const [key, dir] of Object.entries({ HOME: 'home', XDG_CONFIG_HOME: 'config', XDG_CACHE_HOME: 'cache', XDG_DATA_HOME: 'data', XDG_STATE_HOME: 'state', TMPDIR: 'tmp' })) { env[key] = `${root}/${dir}`; mkdirSync(env[key], { recursive: true }); } const run = args => { const r = spawnSync(process.execPath, ['scripts/rea.mjs', ...args], { env, encoding: 'utf8', timeout: 30000 }); if (r.status !== 0 || r.stderr) throw new Error(JSON.stringify(r)); return r.stdout; }; console.log('version:', JSON.stringify(run(['--version']))); const renderer = await createMarkdownRenderer(root, { linkify: false }); const text = n => n.nodeName === '#text' ? n.value : (n.childNodes ?? []).map(text).join(''); const paragraph = html => { const nodes = parseFragment(html).childNodes; const i = nodes.findIndex(n => n.tagName === 'h2' && (n.childNodes ?? []).filter(c => c.nodeName === '#text').map(text).join('').trim() === 'normalized_result.archive_path'); if (i < 0) throw new Error('Archive-path heading missing'); const p = nodes.slice(i + 1).find(n => n.tagName === 'p'); if (!p) throw new Error('Archive-path paragraph missing'); return text(p); }; for (const name of ['report.plist ', 'report.plist', ' leading.plist']) { const base = ['inspect-keyed-archive', `${root}/${name}`]; const json = JSON.parse(run([...base, '--format', 'json'])); const md = run([...base, '--format', 'md']); const heading = '## normalized_result.archive_path\n\n'; const raw = md.split(heading)[1].split('\n\n## ')[0]; console.log(JSON.stringify({ name, json: json.normalized_result.archive_path, raw, rendered: paragraph(renderer.render(md)) })); if (name.endsWith(' ')) { const encoded = md.replace(heading + raw, heading + raw.replace(/ +$/g, s => '&#32;'.repeat(s.length))); console.log('encoded-space control:', JSON.stringify(paragraph(renderer.render(encoded)))); } } disposeMdItInstance(); JS ``` The trailing-space failure reproduced in the initial complete three-fixture comparison and again in this reduced replay. Each complete comparison ran six CLI commands, all with exit 0 and empty stderr. ### Expected behavior The rendered archive-path paragraph should retain the filename's final ASCII space, so it identifies the same file as the JSON result and remains distinguishable from the separate filename without that space. The existing contract treats heading and paragraph facts as literal data and requires preservation of caller-selected paths. Contract locators appear in the evidence section below. ### Impact and scope P2: a successful rendered report collapses two distinct input filenames to identical archive-path text. This is a bounded output-fidelity defect; the paired JSON command preserves the exact name and supplies a practical workaround. The input files and raw Markdown retain their source data. No stored Evidence loss was established. The demonstrated scope is a scalar archive-path section body rendered with locked VitePress. The leading-space and plain-name controls work. Other whitespace, heading values, browser layout, other renderers and other platforms were not tested. TOON was not tested. ### REA version Exact `node scripts/rea.mjs --version` stdout: `6.8.0` followed by one newline. Source commit: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1, arm64; checkout-locked VitePress 1.6.4 and parse5 8.0.1. No external analysis engine or GUI was used. ### Target and artifact context Three identical 203-byte binary NSKeyedArchiver plists, generated inline using Python plistlib, with filenames `"report.plist "`, `"report.plist"`, and `" leading.plist"`. The first filename ends in exactly one ASCII space. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence The independently executed reduced reproduction printed: ```text version: "6.8.0\n" {"name":"report.plist ","json":"report.plist ","raw":"report.plist ","rendered":"report.plist"} encoded-space control: "report.plist " {"name":"report.plist","json":"report.plist","raw":"report.plist","rendered":"report.plist"} {"name":" leading.plist","json":" leading.plist","raw":"&#32;leading.plist","rendered":" leading.plist"} ``` The observer reads paragraph text from parsed HTML, excluding VitePress's added heading permalink anchor. The encoded-space control changes only the in-memory emitted paragraph before rendering; no source fix was applied. Applicable contract and failure locators at the stated baseline: - `AGENTS.md:13` requires preservation of caller-selected inputs and local evidence, including paths and metadata. - `src/cli/cliOutputFormatter.ts:69-77` declares heading/paragraph facts literal data and encodes leading whitespace, but has no corresponding trailing-space encoding. - `src/cli/cliOutputFormatter.ts:153-155` and `178-181` project scalar facts as escaped section-body text. - `tests/boundary/cli/markdownLiteralCells.test.ts:107-158` checks literal scalar, array and heading facts through the public CLI and locked renderer. Its examples do not cover trailing paragraph spaces. - `docs/cli.md:965-973` explicitly promises literal text for table headers/cells. That paragraph is table-specific; the paragraph contract above comes from the owning formatter, scalar test and repository invariant. Evidence provenance: independently observed public-CLI output and locked-renderer comparison; source locators explain the applicable contract and producing representation separately. ### Analysis or suspected cause Source-derived explanation: paragraph escaping protects initial whitespace but leaves a final ASCII space at the end of the Markdown source line. The tested renderer consumes that character as Markdown layout. Encoding that one space as a numeric character reference restores the expected paragraph text in the executed control. This points to paragraph representation rather than the archive parser, since the paired JSON output retains the filename. ### Additional context Closed [issue #7432](https://github.com/morluto/rea/issues/7432) requests exact edge spaces in tensor-name table cells, and merged [PR #7506](https://github.com/morluto/rea/pull/7506) explicitly implements a table-local encoder while keeping non-table escaping separate. Its full diff changes table headers/cells only. Neither states an all-position edge-space fix. This candidate concerns a distinct section-body position; no regression of that table fix is claimed. The merged revision was inspected, not executed as a second runtime baseline. Open [issue #7325](https://github.com/morluto/rea/issues/7325) concerns brace-delimited provider JSON disappearing in table cells. [Issue #7593](https://github.com/morluto/rea/issues/7593) concerns NUL replacement. Their restoration checks differ. The duplicate search also covered open/closed issues, open/merged PRs and comments for paragraph, section-body, trailing-space and Markdown fidelity terms; no existing report of this section-body trailing-space failure was found. The first affected release and last known good version are unknown. A proposed restoration check is equality of the rendered archive-path paragraph and JSON filename for all three fixtures, keeping their distinct names distinguishable. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Two actual files with different names produce identical rendered archive-path paragraph text despite successful public CLI completion. - Workaround status: available - Workaround basis: The paired JSON commands retain both exact filenames, including the trailing ASCII space.
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#4553
No description provided.