[GH-ISSUE #7632] [Bug] A filename beginning with a details fence wraps later Markdown report sections #4559

Closed
opened 2026-10-31 00:50:46 +00:00 by bsakaguchi · 1 comment
Owner

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

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-artifact "$WORK/::: details" --format md succeeds for a ZIP whose filename is the literal text ::: details. The filename appears as a standalone line under subject.name. Rendering the output with the checkout's locked VitePress renderer turns that line into a details container: only two of the report's 31 section headings remain outside it, while 29 subsequent headings become descendants. The generated details element has no open attribute.

The paired JSON command retains subject.name as ::: details and identifies the input as ZIP. The data remains in raw Markdown; its rendered structure changes.

Reproduction

  1. Start at the built baseline checkout below, with its locked dependencies and Node 24.22.1 available as node. This verification used the supplied built runtime and did not install dependencies or rebuild.
  2. Run this self-contained reproduction from the checkout root. It creates the ZIP file, isolates HOME/XDG/temp state, compares actual CLI output with the locked renderer, and removes its generated files:
export WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
export HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config"
export XDG_CACHE_HOME="$WORK/cache" XDG_DATA_HOME="$WORK/data"
export XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/tmp"
mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$TMPDIR"
python3 - "$WORK" <<'PYTHON'
import pathlib, sys, zipfile
with zipfile.ZipFile(pathlib.Path(sys.argv[1]) / '::: details', 'w') as archive:
    archive.writestr('inner.txt', 'hi')
PYTHON
node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { createMarkdownRenderer, disposeMdItInstance } from 'vitepress';
import { parseFragment } from 'parse5';
const root = process.env.WORK;
const env = Object.fromEntries(['PATH', 'HOME', 'XDG_CONFIG_HOME',
  'XDG_CACHE_HOME', 'XDG_DATA_HOME', 'XDG_STATE_HOME', 'TMPDIR']
  .map(key => [key, process.env[key]]));
const run = args => {
  const r = spawnSync(process.execPath, ['scripts/rea.mjs', ...args],
    { env, encoding: 'utf8', timeout: 30000, maxBuffer: 4194304 });
  assert.equal(r.status, 0, r.stderr);
  assert.equal(r.stderr, '');
  return r.stdout;
};
console.log('version:', JSON.stringify(run(['--version'])));
const renderer = await createMarkdownRenderer(root, { linkify: false });
const structure = md => {
  const result = { outside: 0, inside: 0, details: 0, open: false };
  const walk = (node, inside = false) => {
    if (node.tagName === 'details') {
      result.details++;
      result.open ||= node.attrs.some(a => a.name === 'open');
      inside = true;
    }
    if (node.tagName === 'h2') result[inside ? 'inside' : 'outside']++;
    for (const child of node.childNodes ?? []) walk(child, inside);
  };
  walk(parseFragment(renderer.render(md)));
  return result;
};
for (let attempt = 1; attempt <= 3; attempt++) {
  const base = ['inspect-artifact', `${root}/::: details`];
  const md = run([...base, '--format', 'md']);
  const json = JSON.parse(run([...base, '--format', 'json']));
  assert.equal(json.subject.name, '::: details');
  assert.equal(json.subject.format, 'zip');
  assert.ok(md.includes('## subject.name\n\n::: details\n'));
  console.log(JSON.stringify({ attempt, name: json.subject.name,
    rendered: structure(md),
    encodedControl: structure(md.replace(/^::: details$/m, '&#58;:: details')) }));
}
disposeMdItInstance();
JS

The reduced replay reproduced the failure in all three attempts. An earlier independent three-attempt replay produced the same structure. All paired CLI invocations exited 0 with empty stderr.

Expected behavior

The filename should display as the literal text ::: details, with subsequent report sections remaining outside any container created by that data. The scalar-output owner explicitly treats paragraph facts, including block syntax, as literal data. Existing public-CLI tests verify scalar and heading facts using this same VitePress renderer. The applicable contract locators are listed in the evidence section.

The Markdown documentation's literal-display promise is explicitly about table cells; it does not enumerate paragraph container fences. Its description of escaping as a presentation change does not disclaim rendered fidelity for scalar facts.

Impact and scope

P2: a legal filename changes the report's document structure, placing 29 later sections, including the digest, format, provider and normalized result, inside a details element that is closed by default. A reader must expand the unexpected container to see those sections. This default-collapse consequence follows from the verified HTML; no browser layout or GUI was tested.

The paired JSON command retains the exact filename and report data, providing a practical workaround. Raw Markdown also retains the data. Demonstrated scope is one artifact-name scalar paragraph with locked VitePress. Other container types, Markdown renderers, platforms and scalar producers were not tested. No stored Evidence loss or target modification was established.

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 build 26A434, arm64; checkout-locked VitePress 1.6.4 and parse5 8.0.1. No external analysis engine or GUI was used.

Target and artifact context

An independently generated 118-byte ZIP file named ::: details, containing a single regular entry named inner.txt with contents hi. The special text is the ZIP file's name, not an entry name.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

The reduced replay printed:

version: "6.8.0\n"
{"attempt":1,"name":"::: details","rendered":{"outside":2,"inside":29,"details":1,"open":false},"encodedControl":{"outside":31,"inside":0,"details":0,"open":false}}

Attempts 2 and 3 printed identical counts. outside and inside count h2 elements outside and within details containers. Parsed HTML contains <details class="details custom-block"> after the subject.name heading; it has no open attribute. The raw source contains ## subject.name followed by a blank line and ::: details.

Applicable locators at the stated baseline:

  • src/cli/cliOutputFormatter.ts:69-77 declares heading and paragraph facts literal data, including block syntax. Its escape passes leave a leading colon fence unchanged.
  • src/cli/cliOutputFormatter.ts:178-181 projects scalar child values as section-body text through that escaping owner.
  • tests/boundary/cli/markdownLiteralCells.test.ts:107-158 checks public scalar, array and heading output through locked VitePress, including literal block markers and absence of injected list/rule/heading elements. Container fences are absent from its examples.
  • AGENTS.md:13 requires preservation of caller-selected inputs and local evidence, including paths and metadata.
  • docs/cli.md:965-973 promises literal source text in table headers/cells and distinguishes presentation escaping from unchanged Evidence and JSON values. It is narrower than the scalar contract above.

Evidence provenance: independently observed built CLI output and renderer/parsed-HTML structure; the source explanation is separate reasoning. Existing test source was inspected, not run.

Analysis or suspected cause

Source-derived explanation: scalar text is emitted as a Markdown paragraph without protecting the leading colon fence. VitePress recognizes that line as an opening custom container and consumes the following report into it. In the executed control, encoding only the first colon before rendering removed the container and restored all 31 headings outside it. This locates the problem at output representation rather than ZIP inspection; no checkout source was modified.

Additional context

The duplicate search covered open/closed issues, open/merged/closed PRs and comments. Related issue #7625 concerns a trailing space disappearing from a scalar paragraph; its closure signal is literal filename equality, whereas this failure requires preserving report structure around a colon fence. Issue #7325 concerns brace-delimited table JSON, and issue #7440 concerns enum pipes in argument-error tables. Closed issue #7432 and merged PR #7506 cover table edge spaces and explicitly keep non-table escaping separate. These do not report the container failure.

Merged PR #7073 establishes literal inline table rendering. Open PR #7062 retains heading-key regression coverage. Neither describes scalar colon-container escaping. The first affected release and last known good release are unknown.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Successful output for one legal artifact filename places 29 later report sections inside a closed details element in the executed locked-renderer comparison.
  • Workaround status: available
  • Workaround basis: The paired JSON command preserves the exact filename and report data; raw Markdown also retains it.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7632 ### 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-artifact "$WORK/::: details" --format md` succeeds for a ZIP whose filename is the literal text `::: details`. The filename appears as a standalone line under `subject.name`. Rendering the output with the checkout's locked VitePress renderer turns that line into a details container: only two of the report's 31 section headings remain outside it, while 29 subsequent headings become descendants. The generated details element has no `open` attribute. The paired JSON command retains `subject.name` as `::: details` and identifies the input as ZIP. The data remains in raw Markdown; its rendered structure changes. ### Reproduction 1. Start at the built baseline checkout below, with its locked dependencies and Node 24.22.1 available as `node`. This verification used the supplied built runtime and did not install dependencies or rebuild. 2. Run this self-contained reproduction from the checkout root. It creates the ZIP file, isolates HOME/XDG/temp state, compares actual CLI output with the locked renderer, and removes its generated files: ```sh export WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT export HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" export XDG_CACHE_HOME="$WORK/cache" XDG_DATA_HOME="$WORK/data" export XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/tmp" mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$TMPDIR" python3 - "$WORK" <<'PYTHON' import pathlib, sys, zipfile with zipfile.ZipFile(pathlib.Path(sys.argv[1]) / '::: details', 'w') as archive: archive.writestr('inner.txt', 'hi') PYTHON node --input-type=module <<'JS' import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { createMarkdownRenderer, disposeMdItInstance } from 'vitepress'; import { parseFragment } from 'parse5'; const root = process.env.WORK; const env = Object.fromEntries(['PATH', 'HOME', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME', 'XDG_STATE_HOME', 'TMPDIR'] .map(key => [key, process.env[key]])); const run = args => { const r = spawnSync(process.execPath, ['scripts/rea.mjs', ...args], { env, encoding: 'utf8', timeout: 30000, maxBuffer: 4194304 }); assert.equal(r.status, 0, r.stderr); assert.equal(r.stderr, ''); return r.stdout; }; console.log('version:', JSON.stringify(run(['--version']))); const renderer = await createMarkdownRenderer(root, { linkify: false }); const structure = md => { const result = { outside: 0, inside: 0, details: 0, open: false }; const walk = (node, inside = false) => { if (node.tagName === 'details') { result.details++; result.open ||= node.attrs.some(a => a.name === 'open'); inside = true; } if (node.tagName === 'h2') result[inside ? 'inside' : 'outside']++; for (const child of node.childNodes ?? []) walk(child, inside); }; walk(parseFragment(renderer.render(md))); return result; }; for (let attempt = 1; attempt <= 3; attempt++) { const base = ['inspect-artifact', `${root}/::: details`]; const md = run([...base, '--format', 'md']); const json = JSON.parse(run([...base, '--format', 'json'])); assert.equal(json.subject.name, '::: details'); assert.equal(json.subject.format, 'zip'); assert.ok(md.includes('## subject.name\n\n::: details\n')); console.log(JSON.stringify({ attempt, name: json.subject.name, rendered: structure(md), encodedControl: structure(md.replace(/^::: details$/m, '&#58;:: details')) })); } disposeMdItInstance(); JS ``` The reduced replay reproduced the failure in all three attempts. An earlier independent three-attempt replay produced the same structure. All paired CLI invocations exited 0 with empty stderr. ### Expected behavior The filename should display as the literal text `::: details`, with subsequent report sections remaining outside any container created by that data. The scalar-output owner explicitly treats paragraph facts, including block syntax, as literal data. Existing public-CLI tests verify scalar and heading facts using this same VitePress renderer. The applicable contract locators are listed in the evidence section. The Markdown documentation's literal-display promise is explicitly about table cells; it does not enumerate paragraph container fences. Its description of escaping as a presentation change does not disclaim rendered fidelity for scalar facts. ### Impact and scope P2: a legal filename changes the report's document structure, placing 29 later sections, including the digest, format, provider and normalized result, inside a details element that is closed by default. A reader must expand the unexpected container to see those sections. This default-collapse consequence follows from the verified HTML; no browser layout or GUI was tested. The paired JSON command retains the exact filename and report data, providing a practical workaround. Raw Markdown also retains the data. Demonstrated scope is one artifact-name scalar paragraph with locked VitePress. Other container types, Markdown renderers, platforms and scalar producers were not tested. No stored Evidence loss or target modification was established. ### 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 build 26A434, arm64; checkout-locked VitePress 1.6.4 and parse5 8.0.1. No external analysis engine or GUI was used. ### Target and artifact context An independently generated 118-byte ZIP file named `::: details`, containing a single regular entry named `inner.txt` with contents `hi`. The special text is the ZIP file's name, not an entry name. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence The reduced replay printed: ```text version: "6.8.0\n" {"attempt":1,"name":"::: details","rendered":{"outside":2,"inside":29,"details":1,"open":false},"encodedControl":{"outside":31,"inside":0,"details":0,"open":false}} ``` Attempts 2 and 3 printed identical counts. `outside` and `inside` count h2 elements outside and within details containers. Parsed HTML contains `<details class="details custom-block">` after the subject.name heading; it has no open attribute. The raw source contains `## subject.name` followed by a blank line and `::: details`. Applicable locators at the stated baseline: - `src/cli/cliOutputFormatter.ts:69-77` declares heading and paragraph facts literal data, including block syntax. Its escape passes leave a leading colon fence unchanged. - `src/cli/cliOutputFormatter.ts:178-181` projects scalar child values as section-body text through that escaping owner. - `tests/boundary/cli/markdownLiteralCells.test.ts:107-158` checks public scalar, array and heading output through locked VitePress, including literal block markers and absence of injected list/rule/heading elements. Container fences are absent from its examples. - `AGENTS.md:13` requires preservation of caller-selected inputs and local evidence, including paths and metadata. - `docs/cli.md:965-973` promises literal source text in table headers/cells and distinguishes presentation escaping from unchanged Evidence and JSON values. It is narrower than the scalar contract above. Evidence provenance: independently observed built CLI output and renderer/parsed-HTML structure; the source explanation is separate reasoning. Existing test source was inspected, not run. ### Analysis or suspected cause Source-derived explanation: scalar text is emitted as a Markdown paragraph without protecting the leading colon fence. VitePress recognizes that line as an opening custom container and consumes the following report into it. In the executed control, encoding only the first colon before rendering removed the container and restored all 31 headings outside it. This locates the problem at output representation rather than ZIP inspection; no checkout source was modified. ### Additional context The duplicate search covered open/closed issues, open/merged/closed PRs and comments. Related [issue #7625](https://github.com/morluto/rea/issues/7625) concerns a trailing space disappearing from a scalar paragraph; its closure signal is literal filename equality, whereas this failure requires preserving report structure around a colon fence. [Issue #7325](https://github.com/morluto/rea/issues/7325) concerns brace-delimited table JSON, and [issue #7440](https://github.com/morluto/rea/issues/7440) concerns enum pipes in argument-error tables. Closed [issue #7432](https://github.com/morluto/rea/issues/7432) and merged [PR #7506](https://github.com/morluto/rea/pull/7506) cover table edge spaces and explicitly keep non-table escaping separate. These do not report the container failure. Merged [PR #7073](https://github.com/morluto/rea/pull/7073) establishes literal inline table rendering. Open [PR #7062](https://github.com/morluto/rea/pull/7062) retains heading-key regression coverage. Neither describes scalar colon-container escaping. The first affected release and last known good release are unknown. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Successful output for one legal artifact filename places 29 later report sections inside a closed details element in the executed locked-renderer comparison. - Workaround status: available - Workaround basis: The paired JSON command preserves the exact filename and report data; raw Markdown also retains it.
Author
Owner

@coygeek commented on GitHub (Oct 31, 2026):

I checked this against latest main and the issue appears resolved. The problem was that my ZIP file literally named ::: details was emitted as a bare paragraph line under ## subject.name in the Markdown report, and the VitePress renderer read that line as an opening details container, pulling 29 of the 31 section headings — including the digest, format, provider, and normalized result — inside a details element that was closed by default, so those sections were hidden until the container was expanded. That no longer happens because PR #7938, merged into main as commit e124bdfc5, changed the CLI Markdown formatter in src/cli/cliOutputFormatter.ts so that the shared escaping path (escapeTableCell, used by escapeMarkdownText for scalar paragraph text) encodes colons as &#58;. The filename is therefore emitted as &#58;&#58;&#58; details rather than as a fence line, renders as the literal text ::: details, and never opens a container, so the sections that follow it stay outside it as ordinary report sections. The PR also adds a regression test with the ::: details scalar asserting the rendered body is a plain paragraph with the exact source text and no details element. The JSON output was already correct and is unchanged. Closing as resolved.

<!-- gh-comment-id:6537224109 --> @coygeek commented on GitHub (Oct 31, 2026): I checked this against latest main and the issue appears resolved. The problem was that my ZIP file literally named `::: details` was emitted as a bare paragraph line under `## subject.name` in the Markdown report, and the VitePress renderer read that line as an opening details container, pulling 29 of the 31 section headings — including the digest, format, provider, and normalized result — inside a details element that was closed by default, so those sections were hidden until the container was expanded. That no longer happens because [PR #7938](https://github.com/morluto/rea/pull/7938), merged into main as commit [`e124bdfc5`](https://github.com/morluto/rea/commit/e124bdfc54707da138a58ae61b901f0bf93a9bac), changed the CLI Markdown formatter in `src/cli/cliOutputFormatter.ts` so that the shared escaping path (`escapeTableCell`, used by `escapeMarkdownText` for scalar paragraph text) encodes colons as `&#58;`. The filename is therefore emitted as `&#58;&#58;&#58; details` rather than as a fence line, renders as the literal text `::: details`, and never opens a container, so the sections that follow it stay outside it as ordinary report sections. The PR also adds a regression test with the `::: details` scalar asserting the rendered body is a plain paragraph with the exact source text and no details element. The JSON output was already correct and is unchanged. Closing as resolved.
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#4559
No description provided.