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

8.0 KiB

created, status, requirements, system_design, legacy_specs
created status requirements system_design legacy_specs
2026-09-11 blocked
REQ-UI-COMMENT-MARKDOWN-001
../../specs/ui/system-design/comment-markdown.md

Implementation Plan: Bare Markdown Wrappers

Overview

This package records the requested repair, the counterexamples that make raw Markdown wrapper intent ambiguous, and the resulting blocked implementation.

The existing normalizer remains unchanged for bare wrapper candidates. A future implementation needs a producer marker, parser-level intent signal, or another accepted product contract before it can safely repair the reported document.

The analysis contains concrete counterexamples, renderer evidence, and the written-analysis fallback.

Requirement and design reconciliation

UI owns reusable Markdown presentation independently of task or agent state. The existing requirements define GFM rendering under AC-UI-COMMENT-MARKDOWN-001.1 and .2. The existing design names MemoizedMarkdown as the shared renderer.

The existing requirements and design define tagged whole-message wrapper repair, but they do not define a safe raw-Markdown signal for a bare wrapper. The proposed thematic-break contract was rejected because it merges valid independent blocks and can rewrite literal content inside an enclosing code block. No new acceptance criterion or system-design behavior is recorded until that product decision is resolved.

Scope

In scope

  • Record the ambiguity and renderer evidence in the analysis.
  • Add regression coverage for the exact preservation cases.
  • Keep the existing normalizer behavior until a safe contract is accepted.

Out of scope

  • Any bare-wrapper repair without an explicit intent signal.
  • Automatic changes to message producers, renderer APIs, or stored messages.
  • New parser dependencies, cache keys, cache limits, or telemetry.
  • Changes to tagged-wrapper eligibility or existing trailing-prose behavior.
  • Commits, pushes, PRs, and delegated implementation.

Technical approach

normalizeMarkdown calls strengthenMarkdownWrapperFence before its glued-close repair. Tagged wrappers keep their existing whole-message eligibility. Bare wrapper candidates remain unchanged because the raw input does not identify whether matching fences are a wrapper or separate Markdown blocks.

The supplied reproduction has a bare opener and surrounding prose. The normalizer must preserve its current Markdown interpretation until a product-level signal resolves the ambiguity.

Keep any future repair inside apps/web/lib/markdown/normalize-cache.ts. Reuse the existing tagged-wrapper path and do not weaken separate-block preservation. If a future contract is accepted, it must preserve indentation, surrounding prose, interior text, and newline behavior.

normalizeCached remains a raw-input-string LRU with a hard cap of 500 entries. No task, session, message, or viewport information enters its key.

ASCII UI preview

UI-01: Existing chat message, desktop and phone. The requested grouping remains blocked because the raw input has two valid interpretations. The count and content of code blocks are structural requirements, not pixel sizes.

Current interpretation             Requested but unresolved
Intro and rule                    Intro and rule
[code: first portion]             [one code block:
Middle prose                        heading + literal fences
[code: second Go example]            both Go examples + prose]
Final prose                       Rule and outro
[code: rule and outro]

The same ambiguity applies on desktop and phone. This utility introduces no controls, navigation, touch behavior, scroll ownership, or viewport branches. The mobile-parity normalization exception permits targeted renderer coverage in the existing unit test file. No new Playwright file is planned.

Tests

All permanent cases belong in apps/web/lib/markdown/normalize-cache.test.ts. The tests preserve the ambiguous inputs and prove that valid code content remains literal.

Requested behavior Evidence
Bare wrapper candidate with two tagged Go blocks and surrounding prose Byte-identical normalization and rendered Markdown boundaries remain unchanged
Two independent tagged Go blocks Byte-identical normalization and exactly two rendered code blocks with prose between
Existing tagged Markdown wrappers Existing suite passes, including sample boundaries and trailing-prose exclusions
Glued closing fences Standalone func f() {} plus glued fence, nested glued closes, and glued wrapper closes retain existing results
Idempotence Normalize the two-region bare fixture twice and compare with the unchanged first result
Raw-key bounded LRU Existing hit, miss, overflow, and recency tests pass; assert cap equals 500 and distinct raw inputs remain distinct cache entries
Ambiguous syntax samples Rule-bounded independent blocks, five-backtick literal content, and variants remain byte-identical

Also cover CRLF, leading/trailing blank lines, indentation from zero to three spaces, longer runs, missing closes, and bare blocks without nested tags. Reuse fixture constants to respect duplicate-string limits.

Rendered verification

Use React createElement, react-dom/server, and react-markdown with remark-gfm, remark-breaks, and remark-gemoji inside the existing .test.ts. Assert code text and surrounding elements, not only fence counts. This is a parser/render integration check, not a claim of full browser E2E coverage. It exercises the viewport-independent transformation used by desktop and phone.

Work orders

There is one sequential work order.

Verification results

Read-only renderer probes on 2026-09-11 found:

  • Compact reproduction: normalization unchanged, three code blocks.
  • Counterexample A: normalization unchanged, two separate code blocks.
  • Two independent tagged Go blocks: normalization unchanged, two code blocks.
  • Two nested-looking pairs: normalization unchanged, three code blocks.

These probes used installed React and react-markdown packages. The latter two also used all three production remark plugins. Permanent regression tests cover the preserved interpretations in the existing normalizer test file. Documentation validation on 2026-09-11:

  • rtk proxy python3 scripts/lint-spec-files.test.py: 36 tests passed.
  • rtk proxy python3 scripts/lint-spec-files.py --all: all specification files passed.
  • rtk git diff --check -- docs/plans/bare-markdown-wrapper: passed.
  • rtk git status --short -- docs/plans/bare-markdown-wrapper: new package present, untracked.

Verification after restoring the safe boundary on 2026-09-12:

  • pnpm --filter @kandev/web test -- --run lib/markdown/normalize-cache.test.ts: 52 tests passed.
  • pnpm --filter @kandev/web lint: passed with zero warnings.
  • pnpm --filter @kandev/web typecheck: passed.
  • pnpm --filter @kandev/web build:vite: passed. Vite emitted existing chunk-size and ineffective dynamic-import warnings.
  • python3 scripts/lint-spec-files.test.py: 36 tests passed.
  • python3 scripts/lint-spec-files.py --all: all specification files passed.
  • git diff --check: passed.

The full pnpm --filter @kandev/web test suite was started with the configured worker budget but produced no terminal report after 13 minutes, so it was stopped. The focused suite completed independently with a captured exit code of zero.

The package remains uncommitted and blocked pending a product-level intent signal.

Risks and unresolved decision

The thematic-break contract cannot be accepted as a compatibility boundary. A message with the same bytes can represent one wrapper, two independent blocks, or literal content inside a larger fence. Correct longer outer fences or an explicit producer marker remain possible future solutions, but producer changes remain outside this task.