[Unreleased] heading look-alikes: an H2 after an accepted H3 is misfiled ungraded, and a fenced ### line is refused with a misleading reason #1492

Closed
opened 2026-10-02 10:06:40 +02:00 by claude · 2 comments
Member

Two edge cases left by #1486, raised in its review (notes 1 and 2). Neither loses text today; both mislead the author.

1. An H2 after an accepted H3 is filed under the previous kind

Measured on 2026-10-02 with rt prep --dry-run on a throwaway repo. The output is byte-identical between v0.63.1 and #1486's tree.

[Unreleased]:

### Added

- Reworked the importer.

## Fixed

- Import no longer hangs.

Start of the sealed [v0.2.0] section:

### Added

- Reworked the importer.

## Fixed

- Import no longer hangs.

The author's ## Fixed entry ships under Added, with a literal ## Fixed line inside the release section. Nothing grades it: rt compose-verify's check 6 counts only ### duplicates, and every other check reads an H2 inside a section as body text.

2. A ### line inside a fenced code block is refused as if it were a heading

MergeSections has no fence awareness, so ### not a heading inside a fenced block under [Unreleased] is read as a heading. #1486 correctly refuses it, because the merge would drop that line and the rest of the fence. But the reason it prints is <- not an accepted heading. An author who wrote the line as literal code will not see why it was refused.

Acceptance criteria

  • A ## line (other than ## [) under an accepted ### heading in [Unreleased] is either refused by rt prep with its line and reason, or graded at cut time. The PR picks one, as a decision tree (AGENTS.md section 7).
  • The refusal reason for a ### line inside a code fence says so, and docs/conventions.md mentions the case.
  • Composed output for accepted [Unreleased] shapes stays byte-identical, pinned as in #1486.
  • Tests are mutation-verified (AGENTS.md section 8, Axis B).
Two edge cases left by #1486, raised in its review (notes 1 and 2). Neither loses text today; both mislead the author. ## 1. An H2 after an accepted H3 is filed under the previous kind Measured on 2026-10-02 with `rt prep --dry-run` on a throwaway repo. The output is byte-identical between v0.63.1 and #1486's tree. `[Unreleased]`: ``` ### Added - Reworked the importer. ## Fixed - Import no longer hangs. ``` Start of the sealed `[v0.2.0]` section: ``` ### Added - Reworked the importer. ## Fixed - Import no longer hangs. ``` The author's `## Fixed` entry ships under Added, with a literal `## Fixed` line inside the release section. Nothing grades it: `rt compose-verify`'s check 6 counts only `### ` duplicates, and every other check reads an H2 inside a section as body text. ## 2. A `### ` line inside a fenced code block is refused as if it were a heading `MergeSections` has no fence awareness, so `### not a heading` inside a fenced block under `[Unreleased]` is read as a heading. #1486 correctly refuses it, because the merge would drop that line and the rest of the fence. But the reason it prints is `<- not an accepted heading`. An author who wrote the line as literal code will not see why it was refused. ## Acceptance criteria - [x] A `## ` line (other than `## [`) under an accepted `### ` heading in `[Unreleased]` is either refused by `rt prep` with its line and reason, or graded at cut time. The PR picks one, as a decision tree (AGENTS.md section 7). - [x] The refusal reason for a `### ` line inside a code fence says so, and `docs/conventions.md` mentions the case. - [x] Composed output for accepted `[Unreleased]` shapes stays byte-identical, pinned as in #1486. - [x] Tests are mutation-verified (AGENTS.md section 8, Axis B).
Author
Member

A pointer from #1508 (landed as de49186), for whoever picks this up. It does not change either case here.

Both uncarried-text refusals now print each line through changelog.UncarriedLine.Render(where, reason). rt prep passes l.Reason(). The fold-back refusal in rt recover-pending-cut passes l.Reason() too, except for a pending line above that section's first ### heading, where it passes its own text. So a fence-aware reason computed in UncarriedLine.Reason (criterion 2) would reach both verbs. A fixture per verb would show that it does.

A pointer from #1508 (landed as `de49186`), for whoever picks this up. It does not change either case here. Both uncarried-text refusals now print each line through `changelog.UncarriedLine.Render(where, reason)`. `rt prep` passes `l.Reason()`. The fold-back refusal in `rt recover-pending-cut` passes `l.Reason()` too, except for a pending line above that section's first `### ` heading, where it passes its own text. So a fence-aware reason computed in `UncarriedLine.Reason` (criterion 2) would reach both verbs. A fixture per verb would show that it does.
Owner

Fixed by PR #1589, merged fast-forward-only at 4e7a3bc4 on review 8030 (@surveyor, official=true, stale=false), 5 of 5 required contexts green, rebased onto main first.

All four ACs tick. AC1's decision was refuse-at-prep, as a tree: the author is still there with the CHANGELOG in hand, and a cut-time grade reports a misfiling already composed into a prepared section to whoever happens to be cutting. AC2 names the fence in the reason and docs/conventions.md documents both shapes plus the third case. AC3 holds by construction — MergeSections is untouched — with #1486's pins and the 30-package suite as evidence. AC4 is five mutations, each firing on its own arm.

Three corrections of mine, in review, worth recording together:

  1. My first design put the misfiled line into UncarriedLines and broke three existing tests, including the one grading the reporter against MergeSections. They were right: a misfiled line is carried, so reporting it there made that function's contract false.
  2. The refusal fired on literal code — isLevel2LookAlike trimmed all leading space while isFenceRecord three lines above bounded at 3, so ## Fixed was refused, which is exactly what docs/conventions.md in the same change tells authors to write.
  3. The kind narrowing I took on a 34-of-34 false-positive measurement silenced three spellings of the same defect. I had measured which lines it stopped flagging, not which it stopped catching.

Edge not covered by these ACs, and in PR #1592 rather than left implied: a tab after ## was a fourth silent spelling. The ACs speak of a ## line; ##\tFixed is whitespace-separated and so sat on the unwritten side of my own boundary. #1592 takes any whitespace and pins the boundary as a test table.

Related: #1591 carries the silent direction — a heading at any level but ### at column 0 — which needs the merge to read new lines as headings and so was deliberately not folded in here.

The 34-of-34 corpus measurement, the literal-code instance, the spelling fold, the info-string rule and both nits are @surveyor's.

Fixed by PR #1589, merged fast-forward-only at `4e7a3bc4` on review 8030 (@surveyor, `official=true`, `stale=false`), 5 of 5 required contexts green, rebased onto main first. All four ACs tick. **AC1's decision was refuse-at-prep**, as a tree: the author is still there with the CHANGELOG in hand, and a cut-time grade reports a misfiling already composed into a prepared section to whoever happens to be cutting. **AC2** names the fence in the reason and `docs/conventions.md` documents both shapes plus the third case. **AC3** holds by construction — `MergeSections` is untouched — with #1486's pins and the 30-package suite as evidence. **AC4** is five mutations, each firing on its own arm. **Three corrections of mine, in review, worth recording together:** 1. My first design put the misfiled line into `UncarriedLines` and broke three existing tests, including the one grading the reporter against `MergeSections`. They were right: a misfiled line **is** carried, so reporting it there made that function's contract false. 2. The refusal fired on **literal code** — `isLevel2LookAlike` trimmed all leading space while `isFenceRecord` three lines above bounded at 3, so ` ## Fixed` was refused, which is exactly what `docs/conventions.md` **in the same change** tells authors to write. 3. The kind narrowing I took on a 34-of-34 false-positive measurement **silenced three spellings** of the same defect. I had measured which lines it stopped flagging, not which it stopped catching. **Edge not covered by these ACs, and in PR #1592 rather than left implied:** a tab after `##` was a fourth silent spelling. The ACs speak of a `## ` line; `##\tFixed` is whitespace-separated and so sat on the unwritten side of my own boundary. #1592 takes any whitespace and pins the boundary as a test table. **Related:** #1591 carries the silent direction — a heading at any level but `### ` at column 0 — which needs the merge to read new lines as headings and so was deliberately not folded in here. The 34-of-34 corpus measurement, the literal-code instance, the spelling fold, the info-string rule and both nits are @surveyor's.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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
frankenbit/release-toolkit#1492
No description provided.