Skip to content

check-changelog-parity: inline-linked headings are invisible to changelog_versions — absorbing one PASSES (regression vs pre-#2341), and reformatting to that form FALSE-POSITIVES on a required gate #2392

Description

@kyle-sexton

Found by the independent verifier on #2341 (full verdict, 17 fixtures, at pull/2341#issuecomment-5263559579). Filed separately from #2342 because that issue tracks deliberate scope gaps; this is a correctness regression in what #2341 shipped, live on a required gate.

To be clear up front: #2341 is a net improvement and not a revert candidate. It killed the two false positives it targeted, plus an unnamed third on trailing whitespace, and it does not go soft on genuine absorption. This is narrower than what it fixed.

The defect

Routing preservation through changelog_versions made inline-linked headings invisible:

## [1.0.0](https://github.com/o/r/releases/tag/v1.0.0) - 2026-01-01

changelog_versions' trailing anchor is \]?([[:space:]]|$), which does not match when ( immediately follows ]. #2290's ^## \[<ver>\] prefix match did see this form.

1. False negative — a regression vs the pre-fix script

Fork has a linked heading; head absorbs it entirely.

shipped bb817c6c : EXIT=0   <- absorption PASSES
pre-fix bb817c6c^: EXIT=1   <- absorption caught

So for this heading shape, main today is worse than before #2341. Every fixture was run against the pre-fix script as a positive control, so EXIT=0 is distinguishable from "the fixture never reached the check".

2. False positive — on the same gate that accepts the form

Plain ## [1.0.0] reformatted to the linked form, release preserved:

shipped: EXIT=1  ABSORBED CHANGELOG HEADING

And --check-bump accepts the linked form for the bumped version (EXIT=0, no FORMAT failure). So the script simultaneously accepts linked headings and red-lines you for producing one. A PR adding compare links to an old changelog heading is blocked today.

3. Two pre-existing blind spots the same anchor causes — NOT #2341's fault

Recorded because they share a root cause and a fix:

  • --check passes a linked heading naming a version above its manifest, where the plain control gives EXIT=1 CHANGELOG AHEAD OF MANIFEST.
  • --check-order passes a misordered linked heading, where the plain control gives EXIT=1 MISORDERED.

The blind spot, for the third time

The corpus contains zero linked headings. This is the same failure that let #2290 ship: a sweep proves a check is quiet on the corpus, not correct on the format. #2341's own verification was rigorous — 100 commits replayed, 10,233 fork-point headings compared, 0 fires — and could not have caught this, because the shape does not occur here.

Inline-linked headings are a widespread Keep a Changelog convention. They belong in the fixtures whether or not this repo uses them today, exactly as [YANKED] and dated forms did.

Suggested fix

Relax changelog_versions' trailing anchor to accept ( after the closing bracket, rather than giving preservation its own extractor.

The verifier argues the two are not equivalent, and I agree: a separate extractor re-creates the duplicate reader #2341 deliberately deleted, and leaves --check / --check-order blind to the same form. Fixing the shared anchor closes all three at once.

Add fixtures for: linked heading absorbed (must fail), plain→linked reformat (must pass), linked heading with and without a trailing date, and the existing [YANKED] / dated / trailing-whitespace cases so the set covers the documented format rather than the local corpus.

Verification scope, carried from the verdict

Not examined: #2341's 90 new test lines (the verifier's fixtures were independent of them); shellcheck/portability gates; CI wiring; #2342 item 3's fail-open 2>/dev/null fork-point read; duplicate-version fork headings (presence-keying means deleting one of two identical ## [1.0.0] sections passes, though --check-order already rejects duplicates).

Platform delta: all runs on Cygwin bash 5.3.9 with GNU grep/awk/comm; CI is Linux. Both findings above are regex-level and platform-independent. A separate cross-plugin non-finding (head_documents being declare -A'd outside the loop — verified clean in both glob orderings) is the one worth re-confirming on the CI image.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions