Files
kandev/docs/plans/bare-markdown-wrapper/analysis.md
T
2026-09-12 16:29:59 +01:00

5.7 KiB

Bare-wrapper ambiguity analysis

Finding

No content-only heuristic can guarantee both requested outcomes for every valid Markdown message. A bare code block can contain headings and tagged-looking fence lines as literal text. Those characters do not encode the author's wrapper intent.

The work remains blocked. A thematic-break boundary does not establish wrapper intent: the same bounded range can contain two complete independent blocks. No safe content-only discriminator is available for this task.

Source evidence

  • apps/web/lib/markdown/normalize-cache.ts: strengthenMarkdownWrapperFence rejects a bare first opener through MARKDOWN_WRAPPER_OPEN_RE.
  • The same helper uses firstNonBlankLineIndex and lastNonBlankLineIndex. Introductory and trailing prose also prevent detection.
  • markdownWrapperInnerFenceInfo is a heuristic scan, not evidence of author intent. FENCE_OPENER_LINE_RE can match text inside an already open code block.
  • A boundary search without an enclosing-fence parser also sees rules and fence candidates inside a valid five-backtick code block.
  • apps/web/components/shared/memoized-markdown.tsx passes normalizeCached(content) directly to ReactMarkdown.
  • Existing tests preserve Markdown samples followed by separate blocks and preserve tagged wrappers with trailing prose outside the wrapper.

Compact reproduction

Construct the raw string from the following lines. BT3 means exactly three backticks, and BT3go means those backticks immediately followed by go. EMPTY means an empty line. These labels are not part of the input.

Copy everything between the lines.
EMPTY
---
EMPTY
BT3
Open a pull request.
## Why
BT3go
func first() {}
BT3
More prose.
BT3go
func second() {}
BT3
Final prose.
BT3
EMPTY
---
EMPTY
That is self-contained.

Desired interpretation: one raw Markdown code block between the rules. Observed interpretation: three code blocks. The first ends after func first(). The second contains func second(). The third consumes the final rule and outro.

The pure transform leaves this input unchanged. This establishes both the reported symptom and the missing interior-range behavior.

Counterexample A: two complete legitimate blocks

BT3
## Fence syntax
BT3go
func first() {}
BT3
This paragraph separates two examples.
BT3
func second() {}
BT3

The first block shows a Markdown syntax fragment, including a heading and a literal tagged opener. It ends at the first pure fence after func first(). The paragraph is normal text. The second bare block contains func second(). Both blocks have explicit closing fences.

This input satisfies all three suggested signals:

  1. Its first and last nonblank lines are matching bare fences.
  2. A same-length tagged opener and pure closer occur strictly between them.
  3. It contains an ATX heading and prose between code samples.

Strengthening the outermost fences merges two intended code blocks and their separating paragraph. Adding the reproduction's introduction and rules does not remove this ambiguity from an interior-range detector.

Counterexample A2: rule-bounded independent blocks

---
BT3
## Fence syntax
BT3go
func first() {}
BT3
This paragraph separates two examples.
BT3
func second() {}
BT3
---

This input has the proposed thematic-break boundaries, but it is also two complete bare code blocks separated by a paragraph. The first code block contains the literal heading and tagged opener. Strengthening the first and last fences would merge both blocks and the paragraph, so the boundary is not a safe repair signal.

A valid five-backtick text block can contain the complete compact reproduction as literal content. A raw-line scan would find its rules and inner fences and rewrite bytes that Markdown already treats as code text.

Counterexample B: requiring two tagged pairs

BT3
## Fence syntax
BT3go
func first() {}
BT3
More prose.
BT3go
func second() {}
BT3
Final prose.
BT3

Under GFM, this is a bare syntax sample, a separate Go block, and an empty final code block. A fence without an explicit closer extends to the end of the input. Such input is valid Markdown, including during streaming.

The alternative intended interpretation is one wrapper containing two tagged examples. Both interpretations use exactly the same bytes. Requiring two tagged pairs or prose between them cannot distinguish those intentions.

Headings in these examples are meaningful literal documentation. They are not evidence that sibling blocks are nonsensical. Natural-language introduction text and horizontal rules also occur in legitimate example documents.

Read-only verification

Installed normalizeMarkdown, React, and react-markdown produced the following results on 2026-09-11:

Input Normalizer changes bytes Rendered code blocks
Compact reproduction No 3
Counterexample A No 2
Counterexample B No 3
Two independent tagged Go blocks No 2

For counterexample A, the emitted HTML has a paragraph between two pre elements. The first code text contains the literal heading and tagged opener. Counterexample B and the independent Go case also ran with all production remark plugins: GFM, breaks, and gemoji.

Disposition

The proposed boundary contract is rejected. Counterexample A2 proves that it violates the preservation guarantee, and the five-backtick case proves that a boundary scan can rewrite literal code content. The normalizer therefore keeps the existing tagged-wrapper behavior and leaves bare wrapper candidates unchanged. The work order remains blocked until a producer marker, parser-level intent signal, or other product decision supplies information that raw Markdown lacks.