[GH-ISSUE #7634] [Bug] Marker-only filenames become empty ordered list items in rendered Markdown #4560

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

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

Inspecting a valid ZIP whose filename is exactly 1. succeeds, but its filename field becomes document structure when the Markdown is rendered. node scripts/rea.mjs inspect-artifact "$WORK/1." --format md writes 1. under subject.name; the locked VitePress renderer turns that body into an ordered list containing one empty item. The corresponding JSON result retains "name":"1.".

Reproduction

  1. Start in the built baseline checkout with locked dependencies and Node 24.22.1 available as node. Create a ZIP named 1. and isolate local state:
export WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
export HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config"
export XDG_DATA_HOME="$WORK/data" XDG_STATE_HOME="$WORK/state"
export XDG_CACHE_HOME="$WORK/cache" TMPDIR="$WORK/tmp"
mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME" "$TMPDIR"
python3 - "$WORK" <<'PYZIP'
import pathlib, sys, zipfile
with zipfile.ZipFile(pathlib.Path(sys.argv[1]) / '1.', 'w') as archive:
    archive.writestr('inner.txt', 'hi')
PYZIP
  1. Run the CLI and render its actual Markdown output. This comparison reproduced the failure on all three iterations:
node --input-type=module <<'JS'
import { spawnSync } from 'node:child_process';
import { createMarkdownRenderer, disposeMdItInstance } from 'vitepress';
import { parseFragment, serialize } from 'parse5';
const renderer = await createMarkdownRenderer(process.env.WORK, { linkify: false });
const text = n => n.nodeName === '#text' ? n.value :
  (n.childNodes ?? []).map(text).join('');
const run = format => {
  const r = spawnSync(process.execPath, ['scripts/rea.mjs', 'inspect-artifact',
    `${process.env.WORK}/1.`, '--format', format],
    { env: process.env, encoding: 'utf8', timeout: 30000 });
  if (r.status !== 0 || r.stderr) throw new Error(JSON.stringify(r));
  return r.stdout;
};
for (let iteration = 1; iteration <= 3; iteration++) {
  const md = run('md');
  const json = JSON.parse(run('json'));
  const nodes = parseFragment(renderer.render(md)).childNodes;
  const index = nodes.findIndex(n => n.tagName === 'h2' &&
    (n.childNodes ?? []).filter(c => c.nodeName === '#text')
      .map(text).join('').trim() === 'subject.name');
  if (index < 0) throw new Error('Missing subject.name heading');
  const body = nodes.slice(index + 1).find(n => n.tagName);
  console.log(JSON.stringify({ iteration, jsonName: json.subject.name,
    rawBody: md.match(/## subject\.name\n\n([^\n]*)/)?.[1],
    renderedElement: body.tagName, renderedText: text(body),
    innerHTML: serialize(body) }));
}
disposeMdItInstance();
JS

Expected behavior

The filename section should render as literal paragraph text 1.. Scalar facts should preserve their identity instead of being consumed as list syntax. The formatter's explicit paragraph contract and its existing renderer-backed scalar test establish this expectation; exact references appear below. The CLI documentation's literal-text promise explicitly describes tables, so it is supporting context rather than the sole contract for this paragraph.

Impact and scope

P2: a legal filename is replaced by an empty list item in a successful rendered report. The section body's text no longer identifies the source filename. A renderer may draw a generated list ordinal, but the scalar value has become structural markup. JSON and the raw Markdown retain the name and provide observed alternatives.

The public-CLI reproduction covers subject.name from one ZIP filename and the locked VitePress renderer. Separately executed source-formatter controls confirm that 1) also becomes an empty list, while 1. literal, 1) literal, and a table cell containing 1. retain their text. Other CLI fields, platforms and renderers were not exercised. No stored Evidence loss or target mutation was established.

REA version

Exact node scripts/rea.mjs --version stdout, with one trailing newline:

6.8.0

Source commit: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1 build 26A434, arm64. Installed VitePress 1.6.4 and parse5 8.0.1 match the checkout's lockfile. No native analysis provider or GUI was used.

Target and artifact context

An inline-generated 118-byte ZIP named 1., containing inner.txt with the two bytes hi. The CLI recognizes the container by content.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

Each iteration of the CLI/renderer comparison printed the following fields, with its corresponding iteration number:

{"iteration":1,"jsonName":"1.","rawBody":"1.","renderedElement":"ol","renderedText":"\n\n","innerHTML":"\n<li></li>\n"}

All six CLI calls exited 0 with empty stderr. This shows that the successful JSON and raw Markdown preserve the filename, while the first rendered body element contains no filename text.

At the baseline:

  • src/cli/cliOutputFormatter.ts:69–77 defines heading and paragraph facts as literal data, then escapes ordered markers only when whitespace follows their punctuation.
  • src/cli/cliOutputFormatter.ts:153–155 and 176–181 send scalar section bodies through that escaping function.
  • tests/boundary/cli/markdownLiteralCells.test.ts:107–156 checks literal scalar rendering with VitePress and rejects list elements. Its numbered input at line 118 is 1. literal, which has text after the marker. The test does not pin a marker-only scalar.
  • AGENTS.md:13 requires preservation of caller-selected inputs and local evidence, including paths and metadata. docs/cli.md:965–973 describes literal table text and original JSON values.

An attempted npm run test:local -- tests/boundary/cli/markdownLiteralCells.test.ts stopped before executing tests because the runner requires test:focused for that built test project. No passing project-test claim is made; the actual public-CLI comparison above is the reproduction evidence.

Analysis or suspected cause

Source-derived explanation: escapeMarkdownText checks for following whitespace after a leading one-to-nine-digit ordered marker. An end-of-value marker does not meet that condition, so 1. stays unescaped. The executed renderer accepts that form as an empty ordered item. The source formatter also renders 123456789. as an empty list but leaves 1234567890. in a paragraph, consistent with the parser's marker length boundary.

Additional context

Searches covered all issue states, all PR states, and issue comments. Related #7625 concerns trailing-space loss in paragraph values. #7325, #7440, and closed #7432 concern other table-rendering rules. #7592 and #7593 concern character substitution. Their closure checks do not cover a marker-only scalar becoming an empty list. Open PR #7062 retains heading-key regression coverage; merged PR #7073 addresses inline table syntax. Neither presents this scalar input as its defect or closure check. No last known good release was established.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Three successful public-CLI comparisons show a legal filename disappearing from its rendered section body's text because it becomes an empty list item.
  • Workaround status: available
  • Workaround basis: The paired JSON commands and raw Markdown retain the exact filename on every comparison.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7634 ### 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 Inspecting a valid ZIP whose filename is exactly `1.` succeeds, but its filename field becomes document structure when the Markdown is rendered. `node scripts/rea.mjs inspect-artifact "$WORK/1." --format md` writes `1.` under `subject.name`; the locked VitePress renderer turns that body into an ordered list containing one empty item. The corresponding JSON result retains `"name":"1."`. ### Reproduction 1. Start in the built baseline checkout with locked dependencies and Node 24.22.1 available as `node`. Create a ZIP named `1.` and isolate local state: ```sh export WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT export HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" export XDG_DATA_HOME="$WORK/data" XDG_STATE_HOME="$WORK/state" export XDG_CACHE_HOME="$WORK/cache" TMPDIR="$WORK/tmp" mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME" "$TMPDIR" python3 - "$WORK" <<'PYZIP' import pathlib, sys, zipfile with zipfile.ZipFile(pathlib.Path(sys.argv[1]) / '1.', 'w') as archive: archive.writestr('inner.txt', 'hi') PYZIP ``` 2. Run the CLI and render its actual Markdown output. This comparison reproduced the failure on all three iterations: ```sh node --input-type=module <<'JS' import { spawnSync } from 'node:child_process'; import { createMarkdownRenderer, disposeMdItInstance } from 'vitepress'; import { parseFragment, serialize } from 'parse5'; const renderer = await createMarkdownRenderer(process.env.WORK, { linkify: false }); const text = n => n.nodeName === '#text' ? n.value : (n.childNodes ?? []).map(text).join(''); const run = format => { const r = spawnSync(process.execPath, ['scripts/rea.mjs', 'inspect-artifact', `${process.env.WORK}/1.`, '--format', format], { env: process.env, encoding: 'utf8', timeout: 30000 }); if (r.status !== 0 || r.stderr) throw new Error(JSON.stringify(r)); return r.stdout; }; for (let iteration = 1; iteration <= 3; iteration++) { const md = run('md'); const json = JSON.parse(run('json')); const nodes = parseFragment(renderer.render(md)).childNodes; const index = nodes.findIndex(n => n.tagName === 'h2' && (n.childNodes ?? []).filter(c => c.nodeName === '#text') .map(text).join('').trim() === 'subject.name'); if (index < 0) throw new Error('Missing subject.name heading'); const body = nodes.slice(index + 1).find(n => n.tagName); console.log(JSON.stringify({ iteration, jsonName: json.subject.name, rawBody: md.match(/## subject\.name\n\n([^\n]*)/)?.[1], renderedElement: body.tagName, renderedText: text(body), innerHTML: serialize(body) })); } disposeMdItInstance(); JS ``` ### Expected behavior The filename section should render as literal paragraph text `1.`. Scalar facts should preserve their identity instead of being consumed as list syntax. The formatter's explicit paragraph contract and its existing renderer-backed scalar test establish this expectation; exact references appear below. The CLI documentation's literal-text promise explicitly describes tables, so it is supporting context rather than the sole contract for this paragraph. ### Impact and scope P2: a legal filename is replaced by an empty list item in a successful rendered report. The section body's text no longer identifies the source filename. A renderer may draw a generated list ordinal, but the scalar value has become structural markup. JSON and the raw Markdown retain the name and provide observed alternatives. The public-CLI reproduction covers `subject.name` from one ZIP filename and the locked VitePress renderer. Separately executed source-formatter controls confirm that `1)` also becomes an empty list, while `1. literal`, `1) literal`, and a table cell containing `1.` retain their text. Other CLI fields, platforms and renderers were not exercised. No stored Evidence loss or target mutation was established. ### REA version Exact `node scripts/rea.mjs --version` stdout, with one trailing newline: ```text 6.8.0 ``` Source commit: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1 build 26A434, arm64. Installed VitePress 1.6.4 and parse5 8.0.1 match the checkout's lockfile. No native analysis provider or GUI was used. ### Target and artifact context An inline-generated 118-byte ZIP named `1.`, containing `inner.txt` with the two bytes `hi`. The CLI recognizes the container by content. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence Each iteration of the CLI/renderer comparison printed the following fields, with its corresponding iteration number: ```json {"iteration":1,"jsonName":"1.","rawBody":"1.","renderedElement":"ol","renderedText":"\n\n","innerHTML":"\n<li></li>\n"} ``` All six CLI calls exited 0 with empty stderr. This shows that the successful JSON and raw Markdown preserve the filename, while the first rendered body element contains no filename text. At the baseline: - `src/cli/cliOutputFormatter.ts:69–77` defines heading and paragraph facts as literal data, then escapes ordered markers only when whitespace follows their punctuation. - `src/cli/cliOutputFormatter.ts:153–155` and `176–181` send scalar section bodies through that escaping function. - `tests/boundary/cli/markdownLiteralCells.test.ts:107–156` checks literal scalar rendering with VitePress and rejects list elements. Its numbered input at line 118 is `1. literal`, which has text after the marker. The test does not pin a marker-only scalar. - `AGENTS.md:13` requires preservation of caller-selected inputs and local evidence, including paths and metadata. `docs/cli.md:965–973` describes literal table text and original JSON values. An attempted `npm run test:local -- tests/boundary/cli/markdownLiteralCells.test.ts` stopped before executing tests because the runner requires `test:focused` for that built test project. No passing project-test claim is made; the actual public-CLI comparison above is the reproduction evidence. ### Analysis or suspected cause Source-derived explanation: `escapeMarkdownText` checks for following whitespace after a leading one-to-nine-digit ordered marker. An end-of-value marker does not meet that condition, so `1.` stays unescaped. The executed renderer accepts that form as an empty ordered item. The source formatter also renders `123456789.` as an empty list but leaves `1234567890.` in a paragraph, consistent with the parser's marker length boundary. ### Additional context Searches covered all issue states, all PR states, and issue comments. Related [#7625](https://github.com/morluto/rea/issues/7625) concerns trailing-space loss in paragraph values. [#7325](https://github.com/morluto/rea/issues/7325), [#7440](https://github.com/morluto/rea/issues/7440), and closed [#7432](https://github.com/morluto/rea/issues/7432) concern other table-rendering rules. [#7592](https://github.com/morluto/rea/issues/7592) and [#7593](https://github.com/morluto/rea/issues/7593) concern character substitution. Their closure checks do not cover a marker-only scalar becoming an empty list. Open [PR #7062](https://github.com/morluto/rea/pull/7062) retains heading-key regression coverage; merged [PR #7073](https://github.com/morluto/rea/pull/7073) addresses inline table syntax. Neither presents this scalar input as its defect or closure check. No last known good release was established. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Three successful public-CLI comparisons show a legal filename disappearing from its rendered section body's text because it becomes an empty list item. - Workaround status: available - Workaround basis: The paired JSON commands and raw Markdown retain the exact filename on every comparison.
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#4560
No description provided.