B-KP76-backlog-create-rumdl-wrap-heading-corruption
The rumdl@0.2.9 fmt step in apps/sdlc/cli/backlog_cli/create.ts (writeCommitPush) re-wraps body
prose; when a token like #1146 lands at a line start after the wrap, it gets promoted to a
## 1146 ... heading — the sentence is split and a bogus H2 is injected. The file still passes
schema validation and the CLI exits 0, so the corruption ships silently onto the capture PR.
Observed on B-EANZ (captured via sdlc backlog create); repaired by hand on the backlog-capture
branch. Recovery trap: re-running create with the same slug would mint a duplicate id, because the
slug-reuse probe only scans the main checkout’s docs/planning/backlog/ (the file exists only on the
capture branch until merge), and backlog update is frontmatter-only, so there is no CLI path to
fix a corrupted body. Suggested fixes: escape or guard line-start ’#’ during the wrap, or run fmt
before commit with a post-fmt structural diff check (fail if fmt introduced new headings);
additionally make the slug-reuse probe scan the capture branch so re-capture is actually idempotent.
Resolved — the heading structure is compared across the format
Section titled “Resolved — the heading structure is compared across the format”writeCommitPush now records the body’s ATX headings before rumdl fmt, re-reads the file after,
and refuses to commit if they moved. Nothing is staged, committed or pushed on a mismatch, and the
error names the heading that appeared plus the fix (put the token in a link or a code span, or
reword). mintIdentity is a pure scan of the backlog directory, so an aborted run consumes no id —
re-running after an edit is clean.
The comparison is structural, on (depth, text), so whitespace normalisation is not a change while
an invented heading is. #1146 is deliberately not counted as a heading: CommonMark requires a
space after the hashes, which is precisely what MD018 adds to manufacture one.
Two things ruled out on the way. Disabling MD018 for this call site does stop the promotion —
verified — but leaves #1146 at line start, and the commit is not --no-verify, so lefthook’s own
markdown-fmt applies the same rules moments later; it also leaves a landmine for the next fmt of
that file. Escaping the # to \# is durable but has to avoid code spans, link text and URL
fragments, and a bad regex mangling bodies is a worse failure than a loud refusal.
Still open, and deliberately not fixed here: the slug-reuse probe scans only the main checkout’s
docs/planning/backlog/, so re-capturing a slug that exists solely on the capture branch mints a
duplicate id. That was recorded above as the recovery trap for a corrupted body; with the guard in
place no corrupted body ships, so the trap is unreachable by this route, but the idempotency gap
itself is real and independent. It wants a separate change — the probe has to read
origin/backlog-capture, and mintIdentity is a general entity helper shared with the other types.
Re-reviewed against the T-6XZZ-polish-bump-rumdl-formatter binary bump (0.2.65 → 0.2.77): the
fix here is create.ts’s own structural-diff guard, not upstream rumdl behavior, so the guard
holds regardless of the pinned version. Nothing in the 0.2.66–0.2.77 changelog touches MD018 or
line-start #-token handling.