You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
No gate requires a breaking-behavior changeset to carry the breaking annotation at all — the ADR-0087 disposition gate only fires once one already does #8607
Filed unassigned, surfaced while implementing #8411 (docs-only: annotating .changeset/filter-formula-field-refusal.md as breaking). Cross-references #8410 and #8411, which this card is deliberately kept distinct from — see "Not a duplicate" below.
The gap
scripts/check-adr-0087-registration.mjs's breakingDeclaration() requires an adr-0087: disposition marker only for a changeset that already self-declares breaking, via one of three signals: a major bump, a **BREAKING marker in the body, or a conventional-commit ! on the summary line. scripts/check-changeset-no-major.mjs and scripts/check-empty-changeset.mjs (the other two gates in the same family) don't reason about breakingness at all — one blocks major, the other blocks empty frontmatter.
Nothing in the gate family asks "should this changeset have declared itself breaking, but didn't." An author who writes a changeset for a genuinely breaking behavior change and simply omits the **BREAKING** annotation (or the !, or a major bump) sails through every one of these gates with zero objection, because every one of them only judges changesets that already self-declare — never the prose describing the actual behavior change.
Measured
This is exactly what happened. .changeset/filter-formula-field-refusal.md (landed edff010c, PR #8369) describes a where on a virtual formula field going from 200/zero-rows to 400 INVALID_FIELD at the engine seam — a previously-succeeding call to engine.find / findOne / count / aggregate / update / delete now throws. #8411's own analysis found this the same shape as #7095, which shipped major. Before #8411's fix, running node scripts/check-adr-0087-registration.mjs against that changeset reported:
✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (0 non-breaking changeset(s) seen).
No gate anywhere flagged it. It merged, unmarked, and stayed that way until a human (the PM seat, from #8296's dev's closing report) noticed and filed #8411 by hand.
#8410 (closed, fixed by PR #8465) was about derivation — which gates scripts/pm/dispatch-gates.mjs surfaces for a .changeset/ path, so a dev's local loop matches what CI actually runs. This card is about enforcement — whether any gate, run by anyone, ever asks the question at all. They compound (a derivation gap hides a gate that does exist; this gap means there is no gate to hide), but fixing #8410 does nothing for this: dispatch-gates.mjs now correctly surfaces check:adr-0087-registration for a .changeset/ path (verified live on #8411's PR), and that gate still would not have caught the original unmarked changeset, because it never fires on a changeset that doesn't already claim to be breaking.
Why this is likely NOT a small mechanical fix
Detecting "is this changeset breaking" from prose alone is a semantic judgment call, not a pattern match — the same reason #6148's ADR-0087 gate deliberately asks the author in writing rather than inferring. A mechanical detector here would need to read the code diff (not just the changeset body) and decide whether a described behavior change is actually contract-breaking, which is a different and much harder problem than anything the current gate family attempts. Leaving this open rather than prescribing a fix:
Option A — accept no mechanical gate is feasible; rely on PR review / domain-owner sign-off for changes touching known engine/ingress seams.
Option B — a narrower lint that flags changesets whose diff touches specific known-sensitive call sites (validation seams, engine filter/sort/search lowering, etc.) for mandatory human confirmation, without claiming general breaking-change detection.
Whoever picks this up should weigh these on the three axes (real business need / long-term soundness / hard-to-get-wrong for AI-authored changesets) rather than default to building a detector — a false-negative-prone semantic gate that gives false confidence may be worse than the current honestly-absent one.
Refs: #8410 (derivation, fixed), #8411 (the specific unmarked changeset this card generalizes from), #6148 (the ADR-0087 disposition gate's origin and "ask, don't infer" design), #7095 (the sibling engine-seam precedent that shipped major).
Filed unassigned, surfaced while implementing #8411 (docs-only: annotating
.changeset/filter-formula-field-refusal.mdas breaking). Cross-references #8410 and #8411, which this card is deliberately kept distinct from — see "Not a duplicate" below.The gap
scripts/check-adr-0087-registration.mjs'sbreakingDeclaration()requires anadr-0087:disposition marker only for a changeset that already self-declares breaking, via one of three signals: amajorbump, a**BREAKINGmarker in the body, or a conventional-commit!on the summary line.scripts/check-changeset-no-major.mjsandscripts/check-empty-changeset.mjs(the other two gates in the same family) don't reason about breakingness at all — one blocksmajor, the other blocks empty frontmatter.Nothing in the gate family asks "should this changeset have declared itself breaking, but didn't." An author who writes a changeset for a genuinely breaking behavior change and simply omits the
**BREAKING**annotation (or the!, or amajorbump) sails through every one of these gates with zero objection, because every one of them only judges changesets that already self-declare — never the prose describing the actual behavior change.Measured
This is exactly what happened.
.changeset/filter-formula-field-refusal.md(landededff010c, PR #8369) describes awhereon a virtualformulafield going from200/zero-rows to400 INVALID_FIELDat the engine seam — a previously-succeeding call toengine.find/findOne/count/aggregate/update/deletenow throws. #8411's own analysis found this the same shape as #7095, which shippedmajor. Before #8411's fix, runningnode scripts/check-adr-0087-registration.mjsagainst that changeset reported:No gate anywhere flagged it. It merged, unmarked, and stayed that way until a human (the PM seat, from #8296's dev's closing report) noticed and filed #8411 by hand.
Not a duplicate of #8410
#8410 (closed, fixed by PR #8465) was about derivation — which gates
scripts/pm/dispatch-gates.mjssurfaces for a.changeset/path, so a dev's local loop matches what CI actually runs. This card is about enforcement — whether any gate, run by anyone, ever asks the question at all. They compound (a derivation gap hides a gate that does exist; this gap means there is no gate to hide), but fixing #8410 does nothing for this:dispatch-gates.mjsnow correctly surfacescheck:adr-0087-registrationfor a.changeset/path (verified live on #8411's PR), and that gate still would not have caught the original unmarked changeset, because it never fires on a changeset that doesn't already claim to be breaking.Why this is likely NOT a small mechanical fix
Detecting "is this changeset breaking" from prose alone is a semantic judgment call, not a pattern match — the same reason #6148's ADR-0087 gate deliberately asks the author in writing rather than inferring. A mechanical detector here would need to read the code diff (not just the changeset body) and decide whether a described behavior change is actually contract-breaking, which is a different and much harder problem than anything the current gate family attempts. Leaving this open rather than prescribing a fix:
minorwith no breaking annotation — the release digest cannot classify it #8411 exercised.Whoever picks this up should weigh these on the three axes (real business need / long-term soundness / hard-to-get-wrong for AI-authored changesets) rather than default to building a detector — a false-negative-prone semantic gate that gives false confidence may be worse than the current honestly-absent one.
Refs: #8410 (derivation, fixed), #8411 (the specific unmarked changeset this card generalizes from), #6148 (the ADR-0087 disposition gate's origin and "ask, don't infer" design), #7095 (the sibling engine-seam precedent that shipped
major).