Measured while adding --self-test fixtures to scripts/check-changeset-no-major.mjs (#6923). Not fixed there — it is a family-wide change across three scripts with a different consequence in each, so it is out of that PR's fix face. Filed per Prime Directive #10. Unclaimed.
The observation
All three changeset frontmatter parsers share one entry regex:
/^\s*["']?([^"':]+)["']?\s*:\s*([A-Za-z]+)\s*$/
The bump group is anchored ([A-Za-z]+)\s*$, so anything after the bump word defeats the match. A trailing YAML comment is the reachable case.
Measured on origin/main @ d3e53f2d8, by extracting each function's real source text and loading it standalone (no top-level main executed), so this is the shipped code and not a re-implementation:
■ '"@objectstack/spec": major # keep'
check-empty-changeset.declaredBumpsIn : {"fenced":true,"packages":[]} <== judged EMPTY-FRONTMATTER
check-adr-0087.parseChangeset : []
check-changeset-no-major.majorPackagesIn : []
■ control, the same line without the comment
check-empty-changeset.declaredBumpsIn : {"fenced":true,"packages":["@objectstack/spec"]}
check-adr-0087.parseChangeset : [{"pkg":"@objectstack/spec","bump":"major"}]
check-changeset-no-major.majorPackagesIn : ["@objectstack/spec"]
And the ground truth it is measured against — @changesets/parse@0.4.3, the version this repo resolves:
trailing comment => [{"name":"@objectstack/spec","type":"major"}]
So changesets reads it as a real major. All three gates read it as nothing.
Why it is three different bugs, not one
| script |
what it misses |
direction |
check-changeset-no-major.mjs |
the major itself |
false GREEN — a whole-stack major promoted past a guard that printed a tick |
check-empty-changeset.mjs |
the whole declaration ⇒ the file reads as empty-frontmatter |
false RED — a PR that added a perfectly valid changeset is rejected under #5471 |
check-adr-0087-registration.mjs |
signal (1) major in breakingDeclaration |
partial — signals (2) **BREAKING and (3) the ! summary can still carry it |
The false-RED one is the likelier to be hit first (an author writes a comment, the gate tells them their changeset is empty), and the false-GREEN one is the one that matters, because it is silent and it is aimed at the release train.
Extra force behind the "keep them identical" argument
All three functions carry a comment saying the three parsers deliberately read the same block and must agree. That claim is now true for leading blank lines (PR for #6923 aligned the third one) and it is true here too — they agree by all being wrong together. A family-wide fix keeps the claim true; fixing one alone would make it false again, which is why #6923 did not.
Impact, stated honestly
Candidate dispositions (no conclusion presumed)
- Strip a trailing
# comment before matching, in all three parsers, in one PR — plus the fixture in each. check-changeset-no-major.mjs already has a pinned assertion for the current behaviour that is written to be flipped rather than deleted when this lands.
- Tighten instead of loosen: reject a frontmatter line that is entry-shaped but does not fully match, so an unparseable line is loud rather than invisible. Larger blast radius — worth measuring against the stock first.
- Rule the shape out of bounds for authors and say so in the prescription. Cheapest, but it is a convention with no enforcement, which is the class of thing this repo keeps converting into gates.
Leaning 1. Left to triage.
Measured while adding
--self-testfixtures toscripts/check-changeset-no-major.mjs(#6923). Not fixed there — it is a family-wide change across three scripts with a different consequence in each, so it is out of that PR's fix face. Filed per Prime Directive #10. Unclaimed.The observation
All three changeset frontmatter parsers share one entry regex:
/^\s*["']?([^"':]+)["']?\s*:\s*([A-Za-z]+)\s*$/The bump group is anchored
([A-Za-z]+)\s*$, so anything after the bump word defeats the match. A trailing YAML comment is the reachable case.Measured on
origin/main@d3e53f2d8, by extracting each function's real source text and loading it standalone (no top-level main executed), so this is the shipped code and not a re-implementation:And the ground truth it is measured against —
@changesets/parse@0.4.3, the version this repo resolves:So changesets reads it as a real
major. All three gates read it as nothing.Why it is three different bugs, not one
check-changeset-no-major.mjsmajoritselfcheck-empty-changeset.mjscheck-adr-0087-registration.mjsmajorinbreakingDeclaration**BREAKINGand (3) the!summary can still carry itThe false-RED one is the likelier to be hit first (an author writes a comment, the gate tells them their changeset is empty), and the false-GREEN one is the one that matters, because it is silent and it is aimed at the release train.
Extra force behind the "keep them identical" argument
All three functions carry a comment saying the three parsers deliberately read the same block and must agree. That claim is now true for leading blank lines (PR for #6923 aligned the third one) and it is true here too — they agree by all being wrong together. A family-wide fix keeps the claim true; fixing one alone would make it false again, which is why #6923 did not.
Impact, stated honestly
.changeset/pre.jsonis"mode": "pre", so that guard stands aside entirely (Observation:check-changeset-no-major.mjshas no--self-test, and its enforcing half is unexecuted on every run today (pre-mode) #6923).Candidate dispositions (no conclusion presumed)
#comment before matching, in all three parsers, in one PR — plus the fixture in each.check-changeset-no-major.mjsalready has a pinned assertion for the current behaviour that is written to be flipped rather than deleted when this lands.Leaning 1. Left to triage.