bug(changelog): a heading at any level but ### at column 0 ships its entries under the kind above, and nothing reports it #1591

Closed
opened 2026-10-22 02:50:06 +02:00 by quartermaster · 1 comment
Owner

Raised from review 8026 on PR #1589 and deliberately NOT folded into it, because fixing it changes which lines MergeSections treats as headings — i.e. what it EMITS — and #1589's whole argument for touching only the reporter was that the merge grammar stays put, with its own pins.

The shape

classifyMergeRecord requires ### at COLUMN 0. So each of these is a heading to every markdown renderer and ordinary content to the merge:

 ### Fixed      one leading space
# Fixed         level 1
#### Fixed      level 4

The entries beneath them ship under the kind ABOVE, and nothing reports it: not UncarriedLines (the line is carried), not compose-verify's check 6 (column 0 only), and not #1589's new reporter (which keys on ## ). Measured by reading which section heading precedes the entry in the merged output.

Why it is not #1492

#1492's two cases are a ## under an accepted ### and a ### inside a fence. Both were fixable in the REPORTER. This one is not: a ### at indent 1 is only a heading if the merge starts treating it as one, which moves what the merge emits and needs the byte-identical pins re-measured.

Also from the same review

The fence toggle was keyed on "is this a fence" and a nested fence (```md outer, ``` inner) inverted it mid-block. Fixed in #1589 — closesFence now requires the same marker character and a run at least as long, per CommonMark — so that half is closed and recorded here only so the next reader does not re-find it.

Acceptance criteria

  • A heading-looking line at any level other than ### at column 0, under an accepted kind, is either refused with its line and reason or graded at cut time. The PR picks one as a decision tree.
  • The byte-identical pins for accepted [Unreleased] shapes are re-measured, since any fix here changes what MergeSections reads as a heading.
  • Tests are mutation-verified, and at least one arm reads the LANDING SECTION of an entry out of the merged output rather than needling the body.

Anchor

@surveyor, review 8026 on PR #1589. The landing-section measurement method is his, and so is the correction that produced it: his first table computed "do the entries stay under Added" with contains "### Added" && !contains "### Fixed", and ### Fixed is a SUBSTRING of #### Fixed and of ### Fixed, so two rows read false when the kind had not moved at all — the prefix hazard, in the instrument built to demonstrate the prefix hazard.


Closing note — satisfied by PR #1595 @ 011b538

AC1 — REFUSE, not grade. At rt prep time, which is where internal/prep/prep.go already argued the level-2 case belonged in #1492: the author still has the CHANGELOG in hand, while a cut-time grade reports a misfiling to whoever happens to be cutting. The grade branch stays unbuilt.

AC2 — and this AC's own premise was wrong, which is recorded rather than quietly satisfied. It reads "since any fix here changes what MergeSections reads as a heading". It does not: a refusal lives entirely in the reporter, classifyMergeRecord still reads ### at column 0 and nothing else, so what the merge EMITS cannot move. The premise holds only for the grade branch. Pinned anyway, with 19 merge-output pins captured from pre-fix main (1337738) rather than reconstructed afterwards — the ordering is what makes them evidence. @surveyor reproduced it on a wider population: 61 shapes through MergeSections on pre-fix main and on the merged head, byte-identical on all 61.

AC3 — TestMisfiled_1591EveryATXLevel reads the landing section by PARSING the merged output into ### sections, which is the hazard this issue's own anchor names: ### Fixed is a substring of #### Fixed and of ### Fixed.

14 mutants across merge.go and prep.go, one at a time behind a build gate, none inert, unmutated control at 0 applied / 0 red, both files restored byte-identical.

Two shapes the issue did not name

  • ###\tFixed is a level-3 ATX heading at column 0 to CommonMark and TEXT to the merge. The obvious exclusion — "level 3 at column 0 is the accepted one" — would have silenced exactly that shape, so the predicate asks the merge's own prefix constant instead, now shared with classifyMergeRecord.
  • ## Fixed ## is a level-2 heading naming a kind (CommonMark 4.2) and reported 0 — a hole in the level this reporter already claimed.

Deliberately NOT closed here

A kind-naming heading inside a list item, and one written setext, are still reported by nothing — tracked at #1596. Four spaces is an indented code block only at a section's top level; inside a list item the threshold is four columns past the item's content column. The exclusion stays in #1595 as a stated COST, and TestMisfiled_1591IndentedInAListItemIsNotCode pins today's wrong behaviour so the fix reddens rather than silently changes it.

Reviews 8026 (raised), 8034/8036/8038/8040/8042/8044 (the #1592-#1594 arc), 8045/8048/8054/8058 (this PR). @surveyor throughout.

Raised from review 8026 on PR #1589 and deliberately NOT folded into it, because fixing it changes which lines `MergeSections` treats as headings — i.e. what it EMITS — and #1589's whole argument for touching only the reporter was that the merge grammar stays put, with its own pins. ## The shape `classifyMergeRecord` requires `### ` at COLUMN 0. So each of these is a heading to every markdown renderer and ordinary content to the merge: ``` ### Fixed one leading space # Fixed level 1 #### Fixed level 4 ``` The entries beneath them ship under the kind ABOVE, and **nothing reports it**: not `UncarriedLines` (the line is carried), not `compose-verify`'s check 6 (column 0 only), and not #1589's new reporter (which keys on `## `). Measured by reading which section heading precedes the entry in the merged output. ## Why it is not #1492 #1492's two cases are a `## ` under an accepted `### ` and a `### ` inside a fence. Both were fixable in the REPORTER. This one is not: a `### ` at indent 1 is only a heading if the merge starts treating it as one, which moves what the merge emits and needs the byte-identical pins re-measured. ## Also from the same review The fence toggle was keyed on "is this a fence" and a nested fence (```` ```md ```` outer, ``` inner) inverted it mid-block. **Fixed in #1589** — `closesFence` now requires the same marker character and a run at least as long, per CommonMark — so that half is closed and recorded here only so the next reader does not re-find it. ## Acceptance criteria - [x] A heading-looking line at any level other than `### ` at column 0, under an accepted kind, is either refused with its line and reason or graded at cut time. The PR picks one as a decision tree. - [x] The byte-identical pins for accepted `[Unreleased]` shapes are re-measured, since any fix here changes what `MergeSections` reads as a heading. - [x] Tests are mutation-verified, and at least one arm reads the LANDING SECTION of an entry out of the merged output rather than needling the body. ## Anchor @surveyor, review 8026 on PR #1589. The landing-section measurement method is his, and so is the correction that produced it: his first table computed "do the entries stay under Added" with `contains "### Added" && !contains "### Fixed"`, and `### Fixed` is a SUBSTRING of `#### Fixed` and of ` ### Fixed`, so two rows read false when the kind had not moved at all — the prefix hazard, in the instrument built to demonstrate the prefix hazard. --- ## Closing note — satisfied by PR #1595 @ `011b538` **AC1 — REFUSE, not grade.** At `rt prep` time, which is where `internal/prep/prep.go` already argued the level-2 case belonged in #1492: the author still has the CHANGELOG in hand, while a cut-time grade reports a misfiling to whoever happens to be cutting. The grade branch stays unbuilt. **AC2 — and this AC's own premise was wrong, which is recorded rather than quietly satisfied.** It reads *"since any fix here changes what `MergeSections` reads as a heading"*. It does not: a refusal lives entirely in the reporter, `classifyMergeRecord` still reads `### ` at column 0 and nothing else, so what the merge EMITS cannot move. The premise holds only for the grade branch. Pinned anyway, with 19 merge-output pins **captured from pre-fix main (`1337738`)** rather than reconstructed afterwards — the ordering is what makes them evidence. @surveyor reproduced it on a wider population: 61 shapes through `MergeSections` on pre-fix main and on the merged head, **byte-identical on all 61**. **AC3 — `TestMisfiled_1591EveryATXLevel`** reads the landing section by PARSING the merged output into `### ` sections, which is the hazard this issue's own anchor names: `### Fixed` is a substring of `#### Fixed` and of ` ### Fixed`. **14 mutants** across `merge.go` and `prep.go`, one at a time behind a build gate, none inert, unmutated control at 0 applied / 0 red, both files restored byte-identical. ### Two shapes the issue did not name - **`###\tFixed`** is a level-3 ATX heading at column 0 to CommonMark and TEXT to the merge. The obvious exclusion — *"level 3 at column 0 is the accepted one"* — would have silenced exactly that shape, so the predicate asks the merge's own prefix constant instead, now shared with `classifyMergeRecord`. - **`## Fixed ##`** is a level-2 heading naming a kind (CommonMark 4.2) and reported 0 — a hole in the level this reporter already claimed. ### Deliberately NOT closed here A kind-naming heading **inside a list item**, and one written **setext**, are still reported by nothing — tracked at **#1596**. Four spaces is an indented code block only at a section's top level; inside a list item the threshold is four columns past the item's content column. The exclusion stays in #1595 as a stated COST, and `TestMisfiled_1591IndentedInAListItemIsNotCode` pins today's wrong behaviour so the fix reddens rather than silently changes it. Reviews 8026 (raised), 8034/8036/8038/8040/8042/8044 (the #1592-#1594 arc), 8045/8048/8054/8058 (this PR). @surveyor throughout.
Author
Owner

Landed as PR #1595, rebased onto main at 011b538 — a fast-forward, so the landed tree (cf48f00f) is byte-identical to the reviewed one. All three ACs ticked before the merge; approval row 8058 was official=true stale=false and bound to that exact head.

Closing by hand rather than by keyword: the PR body says Refs #1591, because this repo's ac-closure-check regex pairs a close-keyword with a #NNN across markdown block boundaries, so a body discussing fix and #1591 in separate paragraphs can close trackers it never named.

Not closed by this: #1596 — a kind-naming heading inside a list item, and one written setext, are still reported by nothing.

Landed as PR #1595, rebased onto `main` at **`011b538`** — a fast-forward, so the landed tree (`cf48f00f`) is byte-identical to the reviewed one. All three ACs ticked before the merge; approval row 8058 was `official=true stale=false` and bound to that exact head. Closing by hand rather than by keyword: the PR body says `Refs #1591`, because this repo's `ac-closure-check` regex pairs a close-keyword with a `#NNN` across markdown block boundaries, so a body discussing `fix` and `#1591` in separate paragraphs can close trackers it never named. **Not closed by this:** #1596 — a kind-naming heading inside a list item, and one written setext, are still reported by nothing.
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
frankenbit/release-toolkit#1591
No description provided.