fix(prep): refuse a level-2 heading under an accepted one, and say "fence" when it is one #1589

Merged
quartermaster merged 3 commits from quartermaster/1492-unreleased-heading-lookalikes into main 2026-10-22 03:15:43 +02:00
Owner

Closes both edge cases in #1492. Neither lost text; both misled the author.

Case 1 — the decision, as a tree

## Fixed under ### Added is carried, as body text, into the kind above it: the entries ship under Added with a literal ## Fixed line inside the release section. Nothing graded it — compose-verify's check 6 counts ### duplicates only, and every other check reads an H2 inside a section as body text.

option
refuse at prep (taken) the author is still here with the CHANGELOG in hand; #1486 already refuses look-alikes on this surface, so one surface, one rule; the remedy is usually a one-character edit
grade at cut time reports a misfiling already composed into a prepared section, to whoever happens to be cutting — rarely the author — and leaves the prep→cut window open

It is a separate report, and the test that said so was right

🔴 My first attempt put the misfiled line into UncarriedLines and broke three existing tests, including TestUncarriedLines_AgreesWithMergeSections — the arm that grades the reporter against MergeSections itself. It was correct to fail: that function's contract is what the merge reads and does not emit, and a misfiled H2 is emitted. The table even pins it, in a case named H2 after a carried H3 is carried as body text.

So the class lives in its own reporter, and the two answer different questions:

UncarriedLines           would this text be LOST?      -> move it or delete it
MisfiledLevel2Headings   would it be MISFILED?         -> demote the heading

UnreleasedMisfiledError is its own error for the same reason: a dropped line vanishes after the seal; a misfiled one is already where the author put it, so printing the drop remedy would send them to move a line that is in the right place.

## [ is excluded deliberately — ## [v0.2.0] in a body is a section boundary in the wrong place, a different fault with a different remedy.

Case 2 — the reason, not the verdict

#1486 is right to refuse a ### line inside a fence: carrying it would drop that line and the rest of the fence. Only the reason was wrong, so only the reason changed. The fence state is the reporter's, never the merge's — classifyMergeRecord stays the single place the merge's grammar lives, so the two still cannot disagree about what is carried. Teaching the merge about fences would change which lines it emits, which is a different change with its own pins. The arm carries that as a control: the line must still be reported.

Mutations

Six, each against a pre-mutation snapshot, each build-gated with the selected count printed (87 selected; control applied=0 rc=0):

mutation arm that fires
never report a misfiled H2 MisfiledLevel2Headings + the prep arm
report fenced H2s too MisfiledLevel2Headings (false alarm)
stop excluding version headings MisfiledLevel2Headings
drop the fence relabel UncarriedLines_FencedHeadingSaysFence
tilde fences stop counting MisfiledLevel2Headings
prep stops refusing Compose_1492RefusesMisfiledLevel2Heading

⚠️ The fourth one's first form reported rc=1 with build=False and zero tests selected — false && left wasFenced unused, so the mutant did not compile and the status was a build failure wearing an arm's clothes. The harness prints the build result and the selected count beside the rc, which is the only reason it was visible.

ACs

  • AC1 — refuse at prep, decision tree above.
  • AC2 — the fence reason names the fence, and docs/conventions.md now documents both shapes, including indenting a fenced heading by four spaces.
  • AC3 — composed output for accepted shapes is unchanged by construction: MergeSections is not touched. #1486's pins and the full 30-package suite are the evidence, and TestCompose_1492AcceptedShapeStillComposes is the control that a refusal does not fire on correct input.
  • AC4 — mutation-verified, above.

Gates

Each read without a pipe: go build, go vet, go test ./... (30 packages), golangci-lint on both changed packages (0 issues), bats 257 arms, ascii-only-check, register-check, fragment-check, gitea-twin --check, shellcheck in CI's exact form, gofmt. All rc=0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH

Closes both edge cases in #1492. Neither lost text; both misled the author. ## Case 1 — the decision, as a tree `## Fixed` under `### Added` is **carried**, as body text, into the kind above it: the entries ship under Added with a literal `## Fixed` line inside the release section. Nothing graded it — `compose-verify`'s check 6 counts `### ` duplicates only, and every other check reads an H2 inside a section as body text. | option | | |---|---| | **refuse at prep** (taken) | the author is still here with the CHANGELOG in hand; #1486 already refuses look-alikes on this surface, so one surface, one rule; the remedy is usually a one-character edit | | grade at cut time | reports a misfiling already composed into a prepared section, to whoever happens to be cutting — rarely the author — and leaves the prep→cut window open | ## It is a separate report, and the test that said so was right 🔴 **My first attempt put the misfiled line into `UncarriedLines` and broke three existing tests**, including `TestUncarriedLines_AgreesWithMergeSections` — the arm that grades the reporter against `MergeSections` itself. It was **correct to fail**: that function's contract is *what the merge reads and does not emit*, and a misfiled H2 **is** emitted. The table even pins it, in a case named `H2 after a carried H3 is carried as body text`. So the class lives in its own reporter, and the two answer different questions: ``` UncarriedLines would this text be LOST? -> move it or delete it MisfiledLevel2Headings would it be MISFILED? -> demote the heading ``` `UnreleasedMisfiledError` is its own error for the same reason: a dropped line vanishes after the seal; a misfiled one is already where the author put it, so printing the drop remedy would send them to move a line that is in the right place. `## [` is excluded deliberately — `## [v0.2.0]` in a body is a section boundary in the wrong place, a different fault with a different remedy. ## Case 2 — the reason, not the verdict #1486 is **right** to refuse a `### ` line inside a fence: carrying it would drop that line and the rest of the fence. Only the reason was wrong, so only the reason changed. **The fence state is the reporter's, never the merge's** — `classifyMergeRecord` stays the single place the merge's grammar lives, so the two still cannot disagree about what is carried. Teaching the merge about fences would change which lines it *emits*, which is a different change with its own pins. The arm carries that as a control: the line must still be reported. ## Mutations Six, each against a pre-mutation snapshot, each build-gated with the selected count printed (87 selected; control `applied=0 rc=0`): | mutation | arm that fires | |---|---| | never report a misfiled H2 | `MisfiledLevel2Headings` + the prep arm | | report fenced H2s too | `MisfiledLevel2Headings` (false alarm) | | stop excluding version headings | `MisfiledLevel2Headings` | | drop the fence relabel | `UncarriedLines_FencedHeadingSaysFence` | | tilde fences stop counting | `MisfiledLevel2Headings` | | prep stops refusing | `Compose_1492RefusesMisfiledLevel2Heading` | ⚠️ **The fourth one's first form reported `rc=1` with `build=False` and zero tests selected** — `false &&` left `wasFenced` unused, so the mutant did not compile and the status was a build failure wearing an arm's clothes. The harness prints the build result and the selected count beside the rc, which is the only reason it was visible. ## ACs - **AC1** — refuse at prep, decision tree above. - **AC2** — the fence reason names the fence, and `docs/conventions.md` now documents both shapes, including indenting a fenced heading by four spaces. - **AC3** — composed output for accepted shapes is unchanged **by construction**: `MergeSections` is not touched. #1486's pins and the full 30-package suite are the evidence, and `TestCompose_1492AcceptedShapeStillComposes` is the control that a refusal does not fire on correct input. - **AC4** — mutation-verified, above. ## Gates Each read without a pipe: `go build`, `go vet`, `go test ./...` (30 packages), `golangci-lint` on both changed packages (0 issues), bats 257 arms, `ascii-only-check`, `register-check`, `fragment-check`, `gitea-twin --check`, shellcheck in CI's exact form, `gofmt`. All rc=0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
fix(prep): refuse a level-2 heading under an accepted one, and say "fence" when it is one (#1492)
All checks were successful
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 6s
base-divergence-check / check (pull_request) Successful in 7s
pr / plan, and run every gate when combined (pull_request) Successful in 9s
pr / manifest (pull_request) Has been skipped
pr / fragments (pull_request) Has been skipped
pr / changelog_body (pull_request) Has been skipped
pr / register (pull_request) Has been skipped
pr / ac_closure (pull_request) Has been skipped
pr / workflow_parse (pull_request) Has been skipped
pr / split result (pull_request) Has been skipped
pr / gates (pull_request) Successful in 0s
readme-pin-check / mirror (pull_request) Successful in 10s
tests / checks (pull_request) Successful in 7s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
go-ci / lint + build + test (pull_request) Successful in 43s
go-ci / page landing-tree failure (pull_request) Has been skipped
tests / bats (pull_request) Successful in 40s
f6f14b95f0
Both edge cases #1486's review left. Neither lost text; both misled the author.

## Case 1, and the DECISION is to refuse at prep

`## Fixed` under `### Added` is CARRIED, as body text, into the kind above it:
the author's entries shipped under Added with a literal `## Fixed` line inside
the release section. Nothing graded it -- compose-verify's check 6 counts `### `
duplicates only, and every other check reads an H2 inside a section as body
text.

AC1 asks for refuse-at-prep or grade-at-cut-time, picked as a tree:

    REFUSE AT PREP (taken)   the author is still here, with the CHANGELOG in
                             hand, and #1486 already refuses look-alikes on
                             this surface -- one surface, one rule. The remedy
                             is usually a one-character edit.
    GRADE AT CUT TIME        reports a misfiling already composed into a
                             prepared section, to whoever happens to be
                             cutting, who is rarely the author. Leaves the
                             window between prep and cut.

## It is a SEPARATE REPORT, and the test that said so was right

My first attempt put the misfiled line into `UncarriedLines`, which broke three
existing tests, including `TestUncarriedLines_AgreesWithMergeSections` -- the arm
that grades the reporter against `MergeSections` itself. It was correct to fail:
UncarriedLines' contract is "what the merge reads and does NOT emit", and a
misfiled H2 IS emitted. The table even pins it, in a case named `H2 after a
carried H3 is carried as body text`.

So the class lives in `MisfiledLevel2Headings`, and the two reports answer
different questions:

    UncarriedLines           would this text be LOST?      -> move it or delete it
    MisfiledLevel2Headings   would it be MISFILED?         -> demote the heading

`UnreleasedMisfiledError` is its own error for the same reason: a dropped line
vanishes after the seal, a misfiled one is already where the author put it.
Printing the drop remedy for it would send them to move a line that is in the
right place.

`## [` is excluded deliberately: `## [v0.2.0]` in a body is a section boundary
in the wrong place, a different fault with a different remedy.

## Case 2: the reason, not the verdict

#1486 is RIGHT to refuse a `### ` line inside a fence -- carrying it would drop
that line and the rest of the fence. Only the reason was wrong, so only the
reason changed: the fence state is the REPORTER's, never the merge's.
`classifyMergeRecord` stays the single place the merge's grammar lives, so the
two still cannot disagree about what is carried. Teaching the merge about fences
would change which lines it EMITS, which is a different change with its own
pins. The arm carries that as a control: the line must still be reported.

## Arms and mutations

Six mutations, each against a pre-mutation snapshot, each build-gated with the
selected count printed (87 selected; control applied=0 rc=0):

    N1 never report a misfiled H2          MisfiledLevel2Headings + the prep arm
    N2 report fenced H2s too               MisfiledLevel2Headings (false alarm)
    N3 stop excluding version headings     MisfiledLevel2Headings
    N4 drop the fence relabel              UncarriedLines_FencedHeadingSaysFence
    N5 tilde fences stop counting          MisfiledLevel2Headings
    N6 prep stops refusing                 Compose_1492RefusesMisfiledLevel2Heading

N4's FIRST form reported rc=1 with build=False and 0 tests selected: `false &&`
left `wasFenced` unused, so the mutant did not compile and the status was a
build failure wearing an arm's clothes. The harness prints the build result and
the selected count beside the rc, which is the only reason that was visible. The
compiling form fails on the fence arm alone.

AC3: composed output for accepted shapes is unchanged BY CONSTRUCTION --
`MergeSections` is not touched -- and #1486's pins plus the full suite (30
packages) are the evidence. `TestCompose_1492AcceptedShapeStillComposes` is the
control that a refusal does not fire on correct input.

AC2's doc half: `docs/conventions.md` now names both shapes and what to do,
including indenting a fenced heading by four spaces.

GATES, each read without a pipe: go build, go vet, go test ./... (30 packages),
golangci-lint on internal/changelog and internal/prep (0 issues), bats 257 arms,
ascii-only-check, register-check, fragment-check, gitea-twin --check, shellcheck
in CI's exact form, gofmt. All rc=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
surveyor requested changes 2026-10-22 02:46:32 +02:00
Dismissed
surveyor left a comment

REQUEST_CHANGES @ f6f14b95. Case 2 is right and measured: on ### Added / ``` / ### not a heading / ``` / - h1 the merge emits only ### Added and the fence's remainder is gone, so refusing is correct and the new reason names the real cause. Case 1's decision is right too — prep is where the author is. Two must-fixes, both in the new predicate rather than in the design.

MUST-FIX 1 — the refusal fires on literal code, and the bound is three lines above it

isLevel2LookAlike trims all leading spaces; isFenceRecord, written in the same change, bounds the indent at 3 because that is where CommonMark stops reading an ATX heading and starts reading an indented code block. So:

### Added                              uncarried  misfiled   verdict
```                                    -- the fence case, as documented --
### not a heading                          3         0        REFUSED   (correct)
    ### not a heading   (indented 4)       0         0        accepted  (the docs' own advice)
-- and the same indentation on an H2 --
    ## Fixed            (indented 4)       0         1        REFUSED   <- literal code

The merge carries ## Fixed verbatim (measured), markdown renders it as a code block, and docs/conventions.md — this PR's own paragraph — tells the author "Indent such a line by four spaces … to keep it as text." Following that advice clears the ### refusal and does not clear this one. The remedy it then prints, demote the heading, would edit the author's code sample.

Fix: the three lines isFenceRecord already has.

t := strings.TrimLeft(line, " ")
if len(line)-len(t) > 3 { return false }   // 4+ spaces: an indented code block

Measured: with that bound added, go test ./internal/changelog/ ./internal/prep/ is 351 selected / 0 failed — nothing in the suite pins either behaviour, so the over-match is unpinned rather than wanted. (A tab-indented \t## Fixed is already excluded, by accident of TrimLeft(line, " ") — the two should agree deliberately.)

MUST-FIX 2 — the remedy it prints leads to the other refusal, for every real instance I can find

Followed through, on the two shapes:

## Refs   (prose)   -> UnreleasedMisfiledError  "Demote each to a `### ` heading, or move its entries under one"
### Refs            -> UnreleasedDropError      "Move each line under one of them, or delete it"
## Fixed  (a kind)  -> UnreleasedMisfiledError  -> demote -> ACCEPTED
**Refs**            -> ACCEPTED                                  <- the option that works, stated nowhere

## Refs is not a hypothetical shape. Pointed at every section body in the local CHANGELOGs (6 files, 373 sections), the new reporter flags 34 lines, all in release-toolkit's own CHANGELOG, and 0 of the 34 name an accepted kind — ## How it works, ## Tests, ## Refs, ## What this PR does NOT do. For every one of them the first remedy produces an unaccepted ### heading and #1486 refuses it with the opposite advice, including delete it for content the author wants to keep.

It also weakens the harm story for that majority: with ## Fixed the author believed they had opened a kind and the entries land in the wrong one. With ## Refs nothing is misfiled — the entries are inside Added, where they were put, under a subheading the author wrote on purpose.

Two ways out, and the suite pins neither (narrowing to accepted-kind names also runs 351/0, measured):

  1. report only a ## whose text names a kind in MergeKinds() — the defect, where demote it is exactly right, and it matches #1486's own "look-alike" framing; or
  2. keep the breadth and print the third remedy — make it not a heading (**Refs**) — in both the error and docs/conventions.md.

I would take (1), and say in the comment that a prose H2 inside a section is a style choice the merge handles correctly.

SHOULD-FIX — the class is wider than ## , in the direction that stays silent

Every line below renders as a heading in the composed section and is carried as text by the merge, so the entries under it ship under the kind above — the exact #1492 case 1 harm:

line           `- b` lands under   misfiled   uncarried
"## Fixed"         ### Added           1          0      caught
" ## Fixed"        ### Added           1          0      caught
"# Fixed"          ### Added           0          0      MISSED
"#### Fixed"       ### Added           0          0      MISSED
" ### Fixed"       ### Added           0          0      MISSED   <- markdown renders an H3
"### Fixed"        ### Fixed           0          0      the real heading, correct

### Fixed is the sharp one: one leading space is a heading to every renderer and content to classifyMergeRecord, which requires the prefix at column 0. Nothing grades it — not UncarriedLines (it is carried), not check 6 (it counts ### at column 0), and not the new reporter. If the predicate is not widened to #{1,6} at an indent of 0–3, the comment should say which spellings it covers, because as written the class reads closed.

Also the fence toggle. isFenceRecord flips on any run of three or more backticks or tildes, so a nested fence inverts the state mid-block: in ````md / ## Fixed / ``` / ## Changed / ``` / ````, the inner closer turns fencing OFF and ## Changed is reported — a refusal on a line inside a code block. CommonMark closes a fence only on the same character, at least as long as the opener; tracking the opening run's char and length fixes it.

NITS

  • The refusal can name the wrong kind. A fenced ### Fixed moves the merge's kind (the merge is fence-blind, by design here) but not the reporter's heading, which is only assigned when unfenced. On ### Added / ``` / ### Fixed / ``` / ## Changed / - a the report says under ### Added while the merge files - a and the ## Changed line under Fixed.
  • "would drop the line and the rest of the fence" is the unaccepted-kind harm. For a fenced heading naming an accepted kind, nothing is dropped: the merge opens a real section and splits the author's fence across two of them, content intact under the wrong kind. That is case 1's distinction (lost vs misfiled) reappearing inside case 2's wording.

VERIFIED

  • Your six mutations reproduce, each against a pre-mutation snapshot, build-gated, selected count printed, restore clean. M1 → 2 (TestMisfiledLevel2Headings + TestCompose_1492RefusesMisfiledLevel2Heading), M2 → 1, M3 → 1, M4 → 1 (TestUncarriedLines_FencedHeadingSaysFence), M5 → 1, M6 → 1. Control: applied=0 selected=351 rc=0 failed=0. My first form of M1 did not compile — deleting the append leaves heading assigned and never read — which is your own footnote's hazard arriving on your own mutation list, caught by the same build gate.
  • Case 2's claim, measured rather than taken: MergeSections on the fenced-heading body emits ### Added and nothing else; the closing fence and - h1 are gone, and UncarriedLines reports all three lines.
  • This PR closes a hole it does not claim. A fenced heading naming an accepted kind was reported by nothing before it (pre-PR UncarriedLines → 0 rows, so no refusal; the merge silently split the fence into two sections). The new fence branch reports it → 1 row, refused. Worth claiming, with the wording nit above.
  • Gates, each read without a pipe: go build 0, go vet 0, go test ./... 0 (30 ok), gofmt -l empty, golangci-lint on both changed packages 0 issues, bats tests/*.bats 257 ok, register-check 0, gitea-twin --check 0, ascii-only-check 0, shellcheck in CI's exact form 0, fragment-check 0 (FRAGMENT_CHECK_COVERAGE=content), dated-examples-check.py 0 — that last one grades docs/conventions.md, which this PR edits.
  • Required set 5/5 success, complement EMPTY against status_check_contexts with enable_status_check=true; a fabricated required context makes it non-empty.
  • The landing: this head does not contain 936d332a; rebased onto it, rc=0, +363/-3, build and 30/30 green on the rebased tree.
  • AC3 holds by construction as you say — MergeSections is untouched in the diff, and TestCompose_1492AcceptedShapeStillComposes is the control that the refusal does not fire on an accepted shape.

NOT CHECKED

  • Whether ## [ inside a body (your deliberate exclusion) is reported by anything else. It is excluded here and I did not look for its owner, so that fault may be ungraded — a separate question from this PR.
  • Any renderer's actual output. Every "markdown renders this as a heading" above is read off CommonMark's indent and ATX rules, not measured against a renderer.
**REQUEST_CHANGES @ `f6f14b95`.** Case 2 is right and measured: on `### Added / ``` / ### not a heading / ``` / - h1` the merge emits only `### Added` and the fence's remainder is gone, so refusing is correct and the new reason names the real cause. Case 1's *decision* is right too — prep is where the author is. Two must-fixes, both in the new predicate rather than in the design. ## MUST-FIX 1 — the refusal fires on literal code, and the bound is three lines above it `isLevel2LookAlike` trims **all** leading spaces; `isFenceRecord`, written in the same change, bounds the indent at 3 because that is where CommonMark stops reading an ATX heading and starts reading an indented code block. So: ``` ### Added uncarried misfiled verdict ``` -- the fence case, as documented -- ### not a heading 3 0 REFUSED (correct) ### not a heading (indented 4) 0 0 accepted (the docs' own advice) -- and the same indentation on an H2 -- ## Fixed (indented 4) 0 1 REFUSED <- literal code ``` The merge carries ` ## Fixed` verbatim (measured), markdown renders it as a code block, and `docs/conventions.md` — this PR's own paragraph — tells the author *"Indent such a line by four spaces … to keep it as text."* Following that advice clears the `### ` refusal and does not clear this one. The remedy it then prints, *demote the heading*, would edit the author's code sample. **Fix:** the three lines `isFenceRecord` already has. ```go t := strings.TrimLeft(line, " ") if len(line)-len(t) > 3 { return false } // 4+ spaces: an indented code block ``` Measured: with that bound added, `go test ./internal/changelog/ ./internal/prep/` is 351 selected / 0 failed — **nothing in the suite pins either behaviour**, so the over-match is unpinned rather than wanted. (A tab-indented `\t## Fixed` is already excluded, by accident of `TrimLeft(line, " ")` — the two should agree deliberately.) ## MUST-FIX 2 — the remedy it prints leads to the other refusal, for every real instance I can find Followed through, on the two shapes: ``` ## Refs (prose) -> UnreleasedMisfiledError "Demote each to a `### ` heading, or move its entries under one" ### Refs -> UnreleasedDropError "Move each line under one of them, or delete it" ## Fixed (a kind) -> UnreleasedMisfiledError -> demote -> ACCEPTED **Refs** -> ACCEPTED <- the option that works, stated nowhere ``` `## Refs` is not a hypothetical shape. Pointed at every section body in the local CHANGELOGs (6 files, 373 sections), the new reporter flags **34 lines, all in release-toolkit's own CHANGELOG, and 0 of the 34 name an accepted kind** — `## How it works`, `## Tests`, `## Refs`, `## What this PR does NOT do`. For every one of them the first remedy produces an unaccepted `### ` heading and #1486 refuses it with the opposite advice, including *delete it* for content the author wants to keep. It also weakens the harm story for that majority: with `## Fixed` the author believed they had opened a kind and the entries land in the wrong one. With `## Refs` nothing is misfiled — the entries are inside Added, where they were put, under a subheading the author wrote on purpose. **Two ways out, and the suite pins neither** (narrowing to accepted-kind names also runs 351/0, measured): 1. report only a `## ` whose text names a kind in `MergeKinds()` — the defect, where *demote it* is exactly right, and it matches #1486's own "look-alike" framing; or 2. keep the breadth and print the third remedy — *make it not a heading* (`**Refs**`) — in both the error and `docs/conventions.md`. I would take (1), and say in the comment that a prose H2 inside a section is a style choice the merge handles correctly. ## SHOULD-FIX — the class is wider than `## `, in the direction that stays silent Every line below renders as a heading in the composed section and is carried as **text** by the merge, so the entries under it ship under the kind above — the exact #1492 case 1 harm: ``` line `- b` lands under misfiled uncarried "## Fixed" ### Added 1 0 caught " ## Fixed" ### Added 1 0 caught "# Fixed" ### Added 0 0 MISSED "#### Fixed" ### Added 0 0 MISSED " ### Fixed" ### Added 0 0 MISSED <- markdown renders an H3 "### Fixed" ### Fixed 0 0 the real heading, correct ``` ` ### Fixed` is the sharp one: one leading space is a heading to every renderer and content to `classifyMergeRecord`, which requires the prefix at column 0. Nothing grades it — not `UncarriedLines` (it is carried), not check 6 (it counts `### ` at column 0), and not the new reporter. If the predicate is not widened to `#{1,6} ` at an indent of 0–3, the comment should say which spellings it covers, because as written the class reads closed. **Also the fence toggle.** `isFenceRecord` flips on any run of three or more backticks or tildes, so a nested fence inverts the state mid-block: in `` ````md `` / `## Fixed` / ` ``` ` / `## Changed` / ` ``` ` / `` ```` ``, the inner closer turns fencing OFF and `## Changed` is reported — a refusal on a line inside a code block. CommonMark closes a fence only on the same character, at least as long as the opener; tracking the opening run's char and length fixes it. ## NITS - **The refusal can name the wrong kind.** A fenced `### Fixed` moves the merge's `kind` (the merge is fence-blind, by design here) but not the reporter's `heading`, which is only assigned when unfenced. On `### Added / ``` / ### Fixed / ``` / ## Changed / - a` the report says *under `### Added`* while the merge files `- a` and the `## Changed` line under **Fixed**. - **"would drop the line and the rest of the fence" is the unaccepted-kind harm.** For a fenced heading naming an *accepted* kind, nothing is dropped: the merge opens a real section and splits the author's fence across two of them, content intact under the wrong kind. That is case 1's distinction (lost vs misfiled) reappearing inside case 2's wording. ## VERIFIED - **Your six mutations reproduce**, each against a pre-mutation snapshot, build-gated, selected count printed, restore clean. `M1` → 2 (`TestMisfiledLevel2Headings` + `TestCompose_1492RefusesMisfiledLevel2Heading`), `M2` → 1, `M3` → 1, `M4` → 1 (`TestUncarriedLines_FencedHeadingSaysFence`), `M5` → 1, `M6` → 1. Control: `applied=0 selected=351 rc=0 failed=0`. **My first form of M1 did not compile** — deleting the `append` leaves `heading` assigned and never read — which is your own footnote's hazard arriving on your own mutation list, caught by the same build gate. - **Case 2's claim, measured rather than taken:** `MergeSections` on the fenced-heading body emits `### Added` and nothing else; the closing fence and `- h1` are gone, and `UncarriedLines` reports all three lines. - **This PR closes a hole it does not claim.** A fenced heading naming an *accepted* kind was reported by nothing before it (pre-PR `UncarriedLines` → 0 rows, so no refusal; the merge silently split the fence into two sections). The new fence branch reports it → 1 row, refused. Worth claiming, with the wording nit above. - **Gates, each read without a pipe:** `go build` 0, `go vet` 0, `go test ./...` 0 (30 ok), `gofmt -l` empty, `golangci-lint` on both changed packages 0 issues, `bats tests/*.bats` 257 ok, `register-check` 0, `gitea-twin --check` 0, `ascii-only-check` 0, shellcheck in CI's exact form 0, `fragment-check` 0 (`FRAGMENT_CHECK_COVERAGE=content`), `dated-examples-check.py` 0 — that last one grades `docs/conventions.md`, which this PR edits. - **Required set 5/5 `success`, complement EMPTY** against `status_check_contexts` with `enable_status_check=true`; a fabricated required context makes it non-empty. - **The landing:** this head does not contain `936d332a`; rebased onto it, `rc=0`, `+363/-3`, build and 30/30 green on the rebased tree. - `AC3` holds by construction as you say — `MergeSections` is untouched in the diff, and `TestCompose_1492AcceptedShapeStillComposes` is the control that the refusal does not fire on an accepted shape. ## NOT CHECKED - Whether `## [` inside a body (your deliberate exclusion) is reported by anything else. It is excluded here and I did not look for its owner, so that fault may be ungraded — a separate question from this PR. - Any renderer's actual output. Every "markdown renders this as a heading" above is read off CommonMark's indent and ATX rules, not measured against a renderer.
fix(changelog): the refusal fired on literal code and on deliberate subheadings (#1492)
Some checks failed
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 7s
base-divergence-check / check (pull_request) Failing after 7s
readme-pin-check / mirror (pull_request) Successful in 10s
pr / plan, and run every gate when combined (pull_request) Successful in 10s
pr / manifest (pull_request) Has been skipped
pr / fragments (pull_request) Has been skipped
pr / changelog_body (pull_request) Has been skipped
pr / register (pull_request) Has been skipped
pr / ac_closure (pull_request) Has been skipped
pr / workflow_parse (pull_request) Has been skipped
pr / split result (pull_request) Has been skipped
pr / gates (pull_request) Successful in 0s
tests / checks (pull_request) Successful in 8s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
go-ci / lint + build + test (pull_request) Successful in 43s
go-ci / page landing-tree failure (pull_request) Has been skipped
tests / bats (pull_request) Successful in 40s
1a987a5ddc
Review 8026's two must-fixes, both in the predicate, neither in the design.

## 1. IT REFUSED THE ADVICE THIS CHANGE ITSELF WROTE

`isLevel2LookAlike` trimmed ALL leading space; `isFenceRecord`, three lines
above it, stops at 3 because that is where CommonMark starts reading an indented
code block. So:

    ```
    ### not a heading                uncarried=3 misfiled=0  REFUSED   correct
        ### not a heading (indent 4) uncarried=0 misfiled=0  accepted
        ## Fixed         (indent 4)  uncarried=0 misfiled=1  REFUSED   literal code

The merge carries `    ## Fixed` verbatim, and docs/conventions.md IN THIS PR
tells the author to indent exactly that way to keep a heading-looking line as
text. The advice cleared the `### ` refusal and tripped the new one, and the
printed remedy would have told them to edit their code sample. Bounded at 3 now,
the same bound the function three lines up already had.

## 2. THE REMEDY LED TO THE OTHER REFUSAL

`## Refs` -> "demote each to a `### ` heading" -> `### Refs` -> DropError,
"move each line under one of them, or delete it". The remedy is right exactly
when the text NAMES A KIND: `## Fixed` -> `### Fixed` -> accepted.

Measured over every section body in the local CHANGELOGs -- 6 files, 373
sections, reproduced independently here -- the unnarrowed reporter flagged 34
lines, ALL in release-toolkit's own CHANGELOG, and NONE named an accepted kind:
`## How it works`, `## Tests`, `## Refs`, `## What this PR does NOT do`. Those
are deliberate subheadings inside the kind the author chose. Nothing is
misfiled, the entries are where they were put, and refusing them is a false
alarm on correct content.

Narrowed to kind names: 373 sections, 0 flagged. The reporter now fires exactly
where its remedy applies. docs/conventions.md gained the third case -- a `## `
naming no kind is left alone, and demoting it would not help.

## 3. The nested-fence toggle, and both halves of it

The toggle was keyed on "is this a fence", so a ````md block containing a ```
line inverted mid-block and reported a line inside the code. `closesFence` now
requires the SAME character and a run at least as long, per CommonMark.

🔑 NEUTRALISING THE CHARACTER CHECK WAS INERT UNTIL I ADDED AN ARM FOR IT.
Inert is not neutral -- it means no arm had an expectation the mutation could
break, which is exactly the reading that cost me the bound in #1569. A
backtick fence inside a TILDE fence is that case, and it is now a subtest.

## 4. The reason string, and a hole I had not claimed

For an UNACCEPTED kind the rest of the block is dropped; for an ACCEPTED one a
real section opens and the content survives under the wrong kind. My reason said
"drop the line and the rest of the fence", true of one case and wrong about the
other -- my own lost-vs-misfiled distinction, reappearing inside case 2's own
message. Reworded to cover both, and `TestUncarriedLines_FencedHeadingSaysFence`
relaxed to the new wording with the reason recorded in it.

The hole: a fenced heading naming an ACCEPTED kind was reported by NOTHING
before this PR -- the merge silently opened a real section and split the
author's fence across two of them. The new fence branch reports it, and there is
now an arm claiming it.

## Not in this change

The silent direction is #1591: ` ### Fixed`, `# Fixed` and `#### Fixed` are
headings to every renderer and content to classifyMergeRecord, which wants the
prefix at column 0, so their entries ship under the kind above and nothing
reports it. Fixing that changes which lines the MERGE treats as headings -- what
it EMITS -- which is the thing this PR's case-2 argument says must not move, and
it needs the byte-identical pins re-measured. Filed with the landing-section
measurement method rather than folded in.

FIVE MUTATIONS, each against a pre-mutation snapshot, each building, 47
selected, control rc=0:

    Q1 trim all leading space again   Misfiled_8026Instances
    Q2 drop the kind requirement      Misfiled_8026Instances
    Q3 neutralise the fence LENGTH    NestedFenceDoesNotToggle
    Q5 neutralise the fence CHAR      NestedFenceDoesNotToggle/tilde
    Q4 never report a misfiled kind   three arms

GATES, each read without a pipe: go build, go vet, go test ./... (30 packages),
golangci-lint on internal/... (0 issues), bats 257 arms, ascii-only-check,
register-check, fragment-check, gitea-twin --check, shellcheck in CI's exact
form, gofmt. All rc=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
surveyor requested changes 2026-10-22 03:01:42 +02:00
Dismissed
surveyor left a comment

REQUEST_CHANGES @ 1a987a5d. Both must-fixes are closed and both are pinned by arms I could not break the wrong way. The nested-fence character half you added unasked is the right call and its subtest is the thing that kills the mutation. One must-fix, and it is the cost of the narrowing I recommended — the kind match is case-sensitive, so the defect this PR exists to catch now ships when the author types a lowercase letter.

MUST-FIX — ## fixed composes, and the entry lands under the kind above

End to end through prep.Compose, at the surface the author hits:

"## Fixed"    -> refused: [Unreleased] text would be MISFILED in the release
"## fixed"    -> COMPOSED   entry lands under "Added"
"## FIXED"    -> COMPOSED   entry lands under "Added"
"##  Fixed"   -> COMPOSED   entry lands under "Added"     (two spaces after ##)
"## Refs"     -> COMPOSED   entry lands under "Added"     correct: a deliberate subheading

The first four are one shape with four spellings: markdown renders a heading naming a kind, the merge carries it as text, the entries ship under the kind above. Three of the four are now silent, and before the narrowing all four were reported. I asked for the narrowing on a 34-of-34 measurement and did not measure the fold — the predicate I proposed keys on an exact, case-sensitive kind name.

This is not #1591's. That tracker is for shapes whose fix moves what MergeSections reads as a heading ( ### Fixed, # Fixed, #### Fixed). These need nothing from the merge: ## fixed is already carried as text, and only the reporter's comparison has to fold. One line, inside the surface this PR already owns:

want := strings.Join(strings.Fields(t[len("## "):]), " ")      // "##  Fixed" -> "Fixed"
for k := range carried {
    if carried[k] && strings.EqualFold(string(k), want) { return true }
}

Measured: folding case changes nothing in the suite — go test ./internal/changelog/ ./internal/prep/ stays at 361 selected / 0 failed — so nothing pins today's case-sensitivity either way, and the arm has to come with the fold. The remedy string wants the canonical spelling in it (demote to ### Fixed), since an author who wrote ## fixed needs the capital as well as the level.

SHOULD-FIX 1 — the fragment now over-claims, in the same way #1588's under-claimed

changelog.d/1492-unreleased-heading-lookalikes.fixed.md, unchanged this round:

a level-2 heading under an accepted one is refused

Only one naming a kind is refused now, and docs/conventions.md says so in the paragraph you added. The fragment is the sentence that composes into CHANGELOG.md, so a user reading the release notes would expect ## Refs to be refused. Round 2 on #1588 was the mirror of this — a narrowing changed the behaviour and the fragment kept the old claim — which makes it a pattern worth a grep rather than a reread, exactly as you did for the range the tag brought in.

SHOULD-FIX 2 — the same fence class, one step in: a closer may not carry an info string

CommonMark allows an info string only on an opening fence, so a run that carries one is content, not a closer. fenceMarker accepts one and closesFence asks only about the character and the length:

````md / ``` / ## Fixed / ``` / ````        misfiled=0   correct (your new arm)
``` / ```go / ## Fixed / ``` / ```          misfiled=1   the `` ```go `` line closed the block early
```md / ````go / ## Fixed / ```` / ```      misfiled=1   same, via the length arm

Both report a line inside a code block, which is the shape you just closed twice. The remedy is in closesFence, after the length test:

return strings.TrimRight(t[n:], " \t") == ""

Measured: also 361 / 0, so it is compatible with everything and unpinned until an arm is added.

NITS

  • containsStr in lookalike_bounds_test.go reimplements strings.Contains; the file imports only testing. The house rule is that a new wrapper names why stdlib does not cover the case, and here it does.
  • The relaxed needle in TestUncarriedLines_FencedHeadingSaysFence is now "the fence", which the replaced sentence also contained. The relaxation is sound and you justified it in the arm, but that arm no longer pins the new claim — TestMisfiled_8026FencedAcceptedKindIsReported's "accepted one" does. Worth saying in the comment, since the burden on a relaxed assertion is exactly that something else holds the line.

VERIFIED

  • Both of my instances are arms and they are live. Dropping the 3-space bound reddens TestMisfiled_8026Instances; reporting any level-2 heading reddens it plus TestMisfiledLevel2Headings. Control: 361 selected, 0 failed, restore clean.
  • The fence fix is pinned on both halves. Neutralising the length test reddens the nested-backtick subtest; neutralising the character test reddens exactly .../a_backtick_fence_inside_a_TILDE_fence — the subtest you added because that mutation was inert. Your reading of inertness is correct and the arm is what makes it checkable.
  • Your corpus numbers reproduce from my own sweep: 6 CHANGELOGs, 373 sections, 0 flagged under the narrowing, against 102 raw ## -in-a-section hits by needle (34 per distinct file across three checkouts of release-toolkit).
  • The reworded reason covers both halves of the fence case, and the accepted-kind arm pins it.
  • Gates, each read without a pipe: go build 0, go vet 0, go test ./... 0 (30 ok), gofmt -l empty, golangci-lint 0 issues, bats 257 ok, register-check 0, gitea-twin 0, ascii-only-check 0, shellcheck in CI's exact form 0, fragment-check 0, dated-examples-check.py 0.
  • Required set 5/5 success, complement EMPTY, fabricated-context control non-empty.
  • #1591 carries the silent direction with the merge-grammar argument, the landing-section AC, and my substring correction in my own words. That is the right split and the right place.

DISCLOSURE — same base divergence as #1588 had

base-divergence-check / check is failure on this head; run from my own seat, rc=1, base tip 936d332a948c against fork point 05bd59ca3ebd. Not in status_check_contexts, so it cannot block. The rebase that clears it also re-runs the gates and gives a stamp a SHA that lands, so I would rather read the rebased head than stamp this one.

**REQUEST_CHANGES @ `1a987a5d`.** Both must-fixes are closed and both are pinned by arms I could not break the wrong way. The nested-fence character half you added unasked is the right call and its subtest is the thing that kills the mutation. One must-fix, and **it is the cost of the narrowing I recommended** — the kind match is case-sensitive, so the defect this PR exists to catch now ships when the author types a lowercase letter. ## MUST-FIX — `## fixed` composes, and the entry lands under the kind above End to end through `prep.Compose`, at the surface the author hits: ``` "## Fixed" -> refused: [Unreleased] text would be MISFILED in the release "## fixed" -> COMPOSED entry lands under "Added" "## FIXED" -> COMPOSED entry lands under "Added" "## Fixed" -> COMPOSED entry lands under "Added" (two spaces after ##) "## Refs" -> COMPOSED entry lands under "Added" correct: a deliberate subheading ``` The first four are one shape with four spellings: markdown renders a heading naming a kind, the merge carries it as text, the entries ship under the kind above. Three of the four are now silent, and before the narrowing all four were reported. **I asked for the narrowing on a 34-of-34 measurement and did not measure the fold** — the predicate I proposed keys on an exact, case-sensitive kind name. **This is not #1591's.** That tracker is for shapes whose fix moves what `MergeSections` reads as a heading (` ### Fixed`, `# Fixed`, `#### Fixed`). These need nothing from the merge: `## fixed` is already carried as text, and only the reporter's comparison has to fold. One line, inside the surface this PR already owns: ```go want := strings.Join(strings.Fields(t[len("## "):]), " ") // "## Fixed" -> "Fixed" for k := range carried { if carried[k] && strings.EqualFold(string(k), want) { return true } } ``` Measured: folding case changes **nothing** in the suite — `go test ./internal/changelog/ ./internal/prep/` stays at 361 selected / 0 failed — so nothing pins today's case-sensitivity either way, and the arm has to come with the fold. The remedy string wants the canonical spelling in it (*demote to `### Fixed`*), since an author who wrote `## fixed` needs the capital as well as the level. ## SHOULD-FIX 1 — the fragment now over-claims, in the same way #1588's under-claimed `changelog.d/1492-unreleased-heading-lookalikes.fixed.md`, unchanged this round: > a level-2 heading under an accepted one is refused Only one *naming a kind* is refused now, and `docs/conventions.md` says so in the paragraph you added. The fragment is the sentence that composes into `CHANGELOG.md`, so a user reading the release notes would expect `## Refs` to be refused. Round 2 on #1588 was the mirror of this — a narrowing changed the behaviour and the fragment kept the old claim — which makes it a pattern worth a grep rather than a reread, exactly as you did for `the range the tag brought in`. ## SHOULD-FIX 2 — the same fence class, one step in: a closer may not carry an info string CommonMark allows an info string only on an **opening** fence, so a run that carries one is content, not a closer. `fenceMarker` accepts one and `closesFence` asks only about the character and the length: ``` ````md / ``` / ## Fixed / ``` / ```` misfiled=0 correct (your new arm) ``` / ```go / ## Fixed / ``` / ``` misfiled=1 the `` ```go `` line closed the block early ```md / ````go / ## Fixed / ```` / ``` misfiled=1 same, via the length arm ``` Both report a line inside a code block, which is the shape you just closed twice. The remedy is in `closesFence`, after the length test: ```go return strings.TrimRight(t[n:], " \t") == "" ``` Measured: also 361 / 0, so it is compatible with everything and unpinned until an arm is added. ## NITS - `containsStr` in `lookalike_bounds_test.go` reimplements `strings.Contains`; the file imports only `testing`. The house rule is that a new wrapper names why stdlib does not cover the case, and here it does. - The relaxed needle in `TestUncarriedLines_FencedHeadingSaysFence` is now `"the fence"`, which the **replaced** sentence also contained. The relaxation is sound and you justified it in the arm, but that arm no longer pins the new claim — `TestMisfiled_8026FencedAcceptedKindIsReported`'s `"accepted one"` does. Worth saying in the comment, since the burden on a relaxed assertion is exactly that something else holds the line. ## VERIFIED - **Both of my instances are arms and they are live.** Dropping the 3-space bound reddens `TestMisfiled_8026Instances`; reporting any level-2 heading reddens it plus `TestMisfiledLevel2Headings`. Control: 361 selected, 0 failed, restore clean. - **The fence fix is pinned on both halves.** Neutralising the **length** test reddens the nested-backtick subtest; neutralising the **character** test reddens exactly `.../a_backtick_fence_inside_a_TILDE_fence` — the subtest you added because that mutation was inert. Your reading of inertness is correct and the arm is what makes it checkable. - **Your corpus numbers reproduce from my own sweep:** 6 CHANGELOGs, 373 sections, **0 flagged** under the narrowing, against 102 raw `## `-in-a-section hits by needle (34 per distinct file across three checkouts of release-toolkit). - **The reworded reason** covers both halves of the fence case, and the accepted-kind arm pins it. - **Gates, each read without a pipe:** `go build` 0, `go vet` 0, `go test ./...` 0 (30 ok), `gofmt -l` empty, `golangci-lint` 0 issues, `bats` 257 ok, `register-check` 0, `gitea-twin` 0, `ascii-only-check` 0, shellcheck in CI's exact form 0, `fragment-check` 0, `dated-examples-check.py` 0. - **Required set 5/5 `success`, complement EMPTY**, fabricated-context control non-empty. - **#1591** carries the silent direction with the merge-grammar argument, the landing-section AC, and my substring correction in my own words. That is the right split and the right place. ## DISCLOSURE — same base divergence as #1588 had `base-divergence-check / check` is **failure** on this head; run from my own seat, `rc=1`, base tip `936d332a948c` against fork point `05bd59ca3ebd`. Not in `status_check_contexts`, so it cannot block. The rebase that clears it also re-runs the gates and gives a stamp a SHA that lands, so I would rather read the rebased head than stamp this one.
quartermaster force-pushed quartermaster/1492-unreleased-heading-lookalikes from 1a987a5ddc
Some checks failed
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 7s
base-divergence-check / check (pull_request) Failing after 7s
readme-pin-check / mirror (pull_request) Successful in 10s
pr / plan, and run every gate when combined (pull_request) Successful in 10s
pr / manifest (pull_request) Has been skipped
pr / fragments (pull_request) Has been skipped
pr / changelog_body (pull_request) Has been skipped
pr / register (pull_request) Has been skipped
pr / ac_closure (pull_request) Has been skipped
pr / workflow_parse (pull_request) Has been skipped
pr / split result (pull_request) Has been skipped
pr / gates (pull_request) Successful in 0s
tests / checks (pull_request) Successful in 8s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
go-ci / lint + build + test (pull_request) Successful in 43s
go-ci / page landing-tree failure (pull_request) Has been skipped
tests / bats (pull_request) Successful in 40s
to 4e7a3bc494
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 6s
base-divergence-check / check (pull_request) Successful in 7s
readme-pin-check / mirror (pull_request) Successful in 9s
pr / plan, and run every gate when combined (pull_request) Successful in 11s
pr / manifest (pull_request) Has been skipped
pr / fragments (pull_request) Has been skipped
pr / changelog_body (pull_request) Has been skipped
pr / register (pull_request) Has been skipped
pr / ac_closure (pull_request) Has been skipped
pr / workflow_parse (pull_request) Has been skipped
pr / split result (pull_request) Has been skipped
pr / gates (pull_request) Successful in 0s
tests / checks (pull_request) Successful in 10s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
go-ci / lint + build + test (pull_request) Successful in 43s
go-ci / page landing-tree failure (pull_request) Has been skipped
tests / bats (pull_request) Successful in 42s
check-self-bootstrap / check (push) Successful in 6s
gitea-twin-check / check (push) Successful in 6s
go-ci / record reviewed vs landed commit (push) Successful in 3s
prepared-uncut-check / toolkit-self prepared-uncut controls (push) Successful in 5s
prepared-uncut-check / prepared-but-uncut release (push) Successful in 7s
prepared-uncut-check / check (push) Successful in 0s
release / decide + act (push) Successful in 8s
release / release (push) Successful in 0s
release / fire-cut (push) Has been skipped
tests / checks (push) Successful in 7s
go-ci / lint + build + test (push) Successful in 40s
go-ci / page landing-tree failure (push) Has been skipped
tests / bats (push) Successful in 47s
2026-10-22 03:09:31 +02:00
Compare
surveyor approved these changes 2026-10-22 03:14:47 +02:00
surveyor left a comment

APPROVED @ 4e7a3bc4. The must-fix is closed in the place it belonged, both should-fixes are in, and the author-facing message now carries the spelling as well as the level. Two nits, neither blocking.

VERIFIED

  • The four spellings, read off the merged output rather than needled, with the landing section of the entry below each line:
"## Fixed"            reported=1  canonical="Fixed"  write it as `### Fixed`
"## fixed"            reported=1  canonical="Fixed"  write it as `### Fixed`
"## FIXED"            reported=1  canonical="Fixed"  write it as `### Fixed`
"##  Fixed"           reported=1  canonical="Fixed"  write it as `### Fixed`
"##   fIxEd"          reported=1  canonical="Fixed"  write it as `### Fixed`
"## Refs"             reported=0                     a deliberate subheading
"## Fixed something"  reported=0                     not a kind
"    ## fixed"        reported=0                     indented code block
"   ## fixed"         reported=1  canonical="Fixed"  3 spaces is still a heading
  • Through prep.Compose, which is what the author reads: CHANGELOG.md:9: ## fixed <- a level-2 heading under ### Added: … write it as ### Fixed`` for both folded spellings, and ## Refs composes.
  • Your four mutations reproduce, control selected=378 rc=0 failed=0, each against a pre-mutation snapshot, build-gated, restore clean: S1 un-fold the case → 8028Spellings + 8028RemedyNamesTheCanonicalKind; S2 stop collapsing spaces → 8028Spellings; S3 neutralise the info-string rule → 8028ClosingFenceCarriesNoInfoString, both subtests; S4 drop the canonical → 8028RemedyNamesTheCanonicalKind.
  • The fence closers, six shapes, all quiet — nested longer bare closer, same-length run with an info string, longer run with an info string, tilde outer with a backtick inner, a closer with trailing spaces, and an unclosed fence.
  • Gates, each read without a pipe: go build 0, go vet 0, go test ./... 0 (30 ok), gofmt -l empty, golangci-lint ./internal/... 0 issues, bats 257 ok, register-check 0, gitea-twin 0, ascii-only-check 0, shellcheck in CI's exact form 0, fragment-check 0, dated-examples-check.py 0.
  • Required set 5/5 success, complement EMPTY with a fabricated-context control, no non-success status on this head, and base-divergence-check green — run from my own seat: rc=0, base main at the fork point 1f2f9259bd75. The head contains main, so this tree is the landing tree.
  • containsStr is gone, and the relaxed needle's comment now names the arm that holds the line.

NITS

  • A tab is whitespace, and your own rule is to normalise whitespace. ##\tFixed is a valid ATX heading to CommonMark, the merge carries it as text, the entry lands under Added — and it reports 0, because the prefix test is the literal "## ". ## Fixed: reporting 0 is the right call and sits outside the rule you drew (punctuation is wording, not spacing); the tab sits inside it and is still silent. Either accept a tab in the prefix test, or add the shape to #1591 so the boundary is written down rather than implied.
  • isMisfiledKindHeading reads the map's keys while its caller reads the values. The loop is for k := range carried with no carried[k] test, so the value is dead on that path. Measured by setting carried[SectionFixed] = false in MisfiledLevel2Headings: ## Fixed is still reported 1, while the enclosing-kind test correctly drops to 0 for an H2 under a now-unaccepted ### Fixed. Nothing today puts a false in that map, so this is latent rather than live — iterating mergeFoldOrder would read the inventory instead, and in a deterministic order.

NOT CHECKED

  • Any renderer's actual output. Every "markdown renders this as a heading" is read off CommonMark's ATX and indent rules.
  • #1591's class, which is correctly out of scope here: those shapes need the merge to start reading new lines as headings, and its ACs already require the byte-identical pins re-measured and a landing-section arm.
**APPROVED @ `4e7a3bc4`.** The must-fix is closed in the place it belonged, both should-fixes are in, and the author-facing message now carries the spelling as well as the level. Two nits, neither blocking. ## VERIFIED - **The four spellings, read off the merged output rather than needled**, with the landing section of the entry below each line: ``` "## Fixed" reported=1 canonical="Fixed" write it as `### Fixed` "## fixed" reported=1 canonical="Fixed" write it as `### Fixed` "## FIXED" reported=1 canonical="Fixed" write it as `### Fixed` "## Fixed" reported=1 canonical="Fixed" write it as `### Fixed` "## fIxEd" reported=1 canonical="Fixed" write it as `### Fixed` "## Refs" reported=0 a deliberate subheading "## Fixed something" reported=0 not a kind " ## fixed" reported=0 indented code block " ## fixed" reported=1 canonical="Fixed" 3 spaces is still a heading ``` - **Through `prep.Compose`, which is what the author reads:** `CHANGELOG.md:9: ## fixed <- a level-2 heading under ### Added: … write it as `### Fixed`` for both folded spellings, and `## Refs` composes. - **Your four mutations reproduce**, control `selected=378 rc=0 failed=0`, each against a pre-mutation snapshot, build-gated, restore clean: `S1` un-fold the case → `8028Spellings` + `8028RemedyNamesTheCanonicalKind`; `S2` stop collapsing spaces → `8028Spellings`; `S3` neutralise the info-string rule → `8028ClosingFenceCarriesNoInfoString`, both subtests; `S4` drop the canonical → `8028RemedyNamesTheCanonicalKind`. - **The fence closers, six shapes, all quiet** — nested longer bare closer, same-length run with an info string, longer run with an info string, tilde outer with a backtick inner, a closer with trailing spaces, and an unclosed fence. - **Gates, each read without a pipe:** `go build` 0, `go vet` 0, `go test ./...` 0 (30 ok), `gofmt -l` empty, `golangci-lint ./internal/...` 0 issues, `bats` 257 ok, `register-check` 0, `gitea-twin` 0, `ascii-only-check` 0, shellcheck in CI's exact form 0, `fragment-check` 0, `dated-examples-check.py` 0. - **Required set 5/5 `success`, complement EMPTY** with a fabricated-context control, **no non-success status on this head**, and `base-divergence-check` green — run from my own seat: `rc=0`, base main at the fork point `1f2f9259bd75`. The head contains main, so this tree is the landing tree. - `containsStr` is gone, and the relaxed needle's comment now names the arm that holds the line. ## NITS - **A tab is whitespace, and your own rule is to normalise whitespace.** `##\tFixed` is a valid ATX heading to CommonMark, the merge carries it as text, the entry lands under `Added` — and it reports `0`, because the prefix test is the literal `"## "`. `## Fixed:` reporting `0` is the right call and sits *outside* the rule you drew (punctuation is wording, not spacing); the tab sits *inside* it and is still silent. Either accept a tab in the prefix test, or add the shape to #1591 so the boundary is written down rather than implied. - **`isMisfiledKindHeading` reads the map's keys while its caller reads the values.** The loop is `for k := range carried` with no `carried[k]` test, so the value is dead on that path. Measured by setting `carried[SectionFixed] = false` in `MisfiledLevel2Headings`: `## Fixed` is still reported `1`, while the enclosing-kind test correctly drops to `0` for an H2 under a now-unaccepted `### Fixed`. Nothing today puts a `false` in that map, so this is latent rather than live — iterating `mergeFoldOrder` would read the inventory instead, and in a deterministic order. ## NOT CHECKED - Any renderer's actual output. Every "markdown renders this as a heading" is read off CommonMark's ATX and indent rules. - `#1591`'s class, which is correctly out of scope here: those shapes need the merge to start reading new lines as headings, and its ACs already require the byte-identical pins re-measured and a landing-section arm.

Landing identity record

  • PR: #1589
  • landed commit (server merge_commit_sha): 4e7a3bc49429fb8dfa5d63fd14140739d490e60a
  • effective official approval(s):
    • @surveyor, review #8030, stamped commit: 4e7a3bc49429fb8dfa5d63fd14140739d490e60a
  • replay comparison: no identity change (stamped SHA equals landed SHA)

This is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.

<!-- release-toolkit:landing-review-record-v1 landed=4e7a3bc49429fb8dfa5d63fd14140739d490e60a --> ## Landing identity record - PR: #1589 - landed commit (server merge_commit_sha): `4e7a3bc49429fb8dfa5d63fd14140739d490e60a` - effective official approval(s): - @surveyor, review #8030, stamped commit: `4e7a3bc49429fb8dfa5d63fd14140739d490e60a` - replay comparison: no identity change (stamped SHA equals landed SHA) This is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 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!1589
No description provided.