Skip to content

Commit f2b6cf2

Browse files
feat(gates): the changeset level follows the metadata protocol — Metadata-protocol: line, level axis, console refresh at patch, release notes by reader action (ruling A) (#25114)
Fixes #25105 Clause-②: no (tooling and rules text: no published package's accept set or public surface moves) Metadata-protocol: none (the declaration line this PR introduces; nothing a metadata author writes against changes) ## What this PR does Maintainer ruling A (2026-10-21), as the card files it: the changeset level is `minor` only when the **metadata protocol** changes — ① added (a new authorable key, accepted value or enum member, metadata type), ② narrowed or retired, ③ semantic (meaning or default of an authorable key) — and `patch` for everything else, REST, drivers, services, console, `feat:` and `fix!:` alike; non-protocol breaking changes keep their BREAKING banner and ADR-0087 disposition; release notes are grouped by what the reader must do. One PR rather than two: the gate and the rule prose land together, so no in-flight PR meets a level rule AGENTS.md does not state. ### 1. The declaration — a sibling line, not a third arm (`scripts/pm/clause2-line.mjs`) Every PR body and changeset body states its kind on a line of its own beside the `Clause-②:` line: ```text Metadata-protocol: added | narrowed | semantic | none ``` The dev's call the card leaves open is **sibling line**, and the reason is structural: every declaration already written (`Clause-②: no`, `yes (widening)`, `no (narrowing)`) keeps reading byte-identically — `readClause2Line`'s predicate, value class and arm reader are untouched, and the six existing readers (`check-adr-0087-registration`, `check-changeset-no-major`, `objectui-changeset-digest`, `pm/check-half-states`, `pm/post-stamped`, `pm/record-recognisers`) see exactly the readings they saw before. `readMetadataProtocolLine` reuses the same key-line shape, decoration tolerance, value-token calibration (first token after the colon, trailing reasoning accepted, an alternation or an unfilled template `malformed`) and the same describing / inline-key / spelling near-miss triage, parameterised on the key and the closed value set, so the two lines cannot drift in what they accept. The module still imports nothing (both gates stage it into a throwaway repo and the level gate runs before `pnpm install`). One token per line; where several kinds apply the author names the highest in breaking-ness, `narrowed` over `semantic` over `added`. ### 2. The level axis (`scripts/check-changeset-no-major.mjs`) Both lines are read from the PR body (the event payload, no token, re-read on `edited`), exactly where the `Clause-②:` line was read. The verdicts, every one with its own sentence and its own one-line check-run annotation: | Reading | Verdict | Exit | |:--|:--|:--| | a kind (`added` / `narrowed` / `semantic`) and at least one moved package graded `minor`+ | `clean` / `discharged` (#16361 grain, unchanged) | 0 | | a kind, moved packages graded `patch`, none raised | `enforce` | 1 | | `none`, no introduced entry above `patch`, no baseline moved | `none-clean` — the BREAKING carriers are stated as untouched | 0 | | `none` with any introduced `minor`/`major`, on **any** package | `none-minor` | 1 | | `none` while `packages/spec/authorable-surface/` or `authorable-defaults/` shards moved | `none-baseline-moved`, naming the shard and the kinds that would be true | 1 | | `added` under `Clause-②: no`; `narrowed` beside a line with no `(narrowing)` arm | `contradiction` (two reasons, two remedies) | 1 | | the kind unreadable and material — a `patch` on a moved package, any `minor`, or a moved baseline | `not-measured-material`, naming which line(s) are missing and spelling all four kinds | 1 | | the kind unreadable and nothing a kind or a `none` would grade differently | `not-measured-moot` | 0 | What is **not** loosened: the `Clause-②:` line unreadable over a moved package graded `patch` with no raise is still `not-measured-material` (the #16776 obligation stays where it was); the major guard, the protocol-lockstep evidence, the grain (#16361), the depth (#16713), the root (#16692) and the copy (#15204) legs are inherited unchanged, their batteries re-driven with `Metadata-protocol: added` where they drove `Clause-②: yes`. What is re-ruled: `Clause-②: yes` or a `(narrowing)` arm no longer couples to `minor` — a REST widening or a driver-posture narrowing under `none` is `none-clean` at `patch`. Neither line is inferred from the other: a `yes` is not read as `added`, an arm is not read as `narrowed`; the contradictions go the other way only. `not-declared` is gone rather than renamed: under ruling A a bare `no` decides nothing about the level. New: a ruling-A battery (37 assertions, floor 30) drives the real reader and real temp repositories; the roster floor is 21 → 22. Self-test: 455 assertions. ### 3. Readers of the one reader, touched only as far as the extension requires - `scripts/check-adr-0087-registration.mjs`: `Metadata-protocol: narrowed` is the **fifth** breaking signal (a protocol narrowing is a break by definition, and ruling A keeps the carriers on it); `added`, `semantic`, `none` and a malformed kind add no signal — convention-rot fixtures pin both directions. Self-test: 548 assertions. - Untouched, by measurement: `pm/record-recognisers.mjs` (the review face keys on the `Clause-②` arm, which keeps its meaning), `pm/post-stamped.mjs` (claim keyed-line refusals; the claim template is out of this claim), `pm/check-half-states.mjs` (reads the claim's `Clause-②:` only). ### 4. Console refresh grades `patch` (`scripts/objectui-changeset-digest.mjs`) The pin-bump changeset is `patch` whatever objectui declared over the range — in RC pre-mode too, since the console never changes the metadata protocol and the platform's version line follows it alone. `declaredLevel` is still derived and reported (the entry lines carry it, the body says "declared `X`, recorded as `patch`" when they differ), `CONSOLE_BUMP` still overrides, and the generated file carries `Metadata-protocol: none` so the gate and the release notes read it. The #14178 deepen test's discriminator moved from the bump word to the record itself (breaking entry, ADR-0087 prompt, no degraded list), which is what that card was about. Self-test: all checks passed (end-to-end through `bump-objectui.sh` and through the real ADR-0087 gate). ### 5. Release notes grouped by what the reader must do (`scripts/release-github-releases.mjs`) The mechanism: the existing GitHub Release body path. `changeset version` keeps writing CHANGELOG.md as it does (RELEASE-OWNED, untouched); `planRelease` now posts that section **regrouped** — the same bullets, byte for byte, none dropped — under the five headings in the ruling's order, each read from the declarations the entry carries: 需手动处理 (breaking — `narrowed`, the `(narrowing)` arm, a BREAKING banner or a `!` summary — with no `registered` ADR-0087 conversion), 自动迁移 (breaking with a `registered` conversion `objectstack migrate meta` applies), 新增能力 (`added`, or a non-breaking `Clause-②: yes` widening), 语义变化 (`semantic`), 其他修复 (the rest: `none`, dependency bumps, the pre-rule stock). Empty sections are omitted; the truncation path is the same as before. The splitter is fence-aware and only a column-0 `- ` opens a bullet, so an entry's own FROM → TO tables, nested lists and fenced examples stay inside it (pinned on the real `16.1.0` section: same bullet count before and after). The breaking signals mirror the ADR-0087 gate rather than importing an 8,000-line module into the release lane. Self-test: 103 assertions, floor 14 → 15. `annotate-shipped` (`release-pending-publish.mjs`) was measured and is not the release-notes path — it writes "Shipped in" lead lines into unconsumed changesets — so it is untouched. The docs release page is curated by hand (`check-release-section-coverage` generates nothing on purpose); `docs/releases-maintenance.md` now says a curated section uses the same five headings. ### 6. The prose (Tier H) - `AGENTS.md` Post-Task Checklist item 3 restated from the ruling's table — **line-neutral at 1117** (the ratchet's ceiling, headroom 0): the paragraph stays 18 lines at ≤119 bytes by condensing its own retirement-kit sentences (the detail stays in the `spec-property-retirement` skill it already defers to); no re-wrap elsewhere, no ceiling raise asked. - `.github/workflows/pr-automation.yml` "WHICH LEVEL" (inside the quoted heredoc of `Require a changeset`): the table, the `.strict()` rationale, the carriers, the gate's verdicts. The step's own `::error::` annotation, `$MERGE_BASE` wiring and trigger types are untouched (the self-tests that pin them are green). - `docs/releases-maintenance.md`: the console grade and the five sections. ## Measured against the dispatch hypotheses - **H1** (re-measured at `5050442bb9`): `.changeset/config.json` fixed group as described; the AGENTS.md `Clause-②` sentence sat at `:1087–1088`, "WHICH LEVEL" at `:754`; `authorable-surface/` is 14 shards and `authorable-defaults/` 13, key names and defaults only — the card's reading holds. - **H2** (readers, `git grep clause2-line`): exactly the six files the card names. The import-free property is kept; every existing reader's verdict on today's spellings is unchanged (the Clause-② reader's code path is byte-identical in behaviour; its three generalised helpers default to their old arguments). - **H3** (who depends on `yes ⇒ minor`): one mechanism — `judgeLevel` in `check-changeset-no-major.mjs` (and the prose that restates it: `renderLevel`, "WHICH LEVEL", AGENTS.md). `record-recognisers` (review face) and the ADR-0087 gate read the arm, not the level; the digest's `major ⇒ minor` downgrade was the sibling coupling and is now `⇒ patch`. The `Clause-②` value keeps its meaning everywhere else. - **H4** (transition, read-only over the 12 open PRs' bodies and the 13 changeset files they introduce): 2 PRs introduce no changeset (the Version Packages PR and the `skip-changeset` shard-timings PR) and are unaffected. The other **10 all turn `not-measured-material`** on landing, because none carries a `Metadata-protocol:` line and each introduces an entry the kind decides on; one PR-body line clears the reading with no push. Declared truthfully (my reading, for the seat and the authors, not a verdict): 3 keep `minor` as protocol changes (#24947 `added`; #25074 and #25052 `narrowed`); 2 are `patch`-only and clear with `none` (#25103, #25100); 4 carry a non-protocol `minor` that ruling A grades `patch` and would read `none-minor` until regraded (#25101 posture, #25086 runtime refusal, #24940 and #24893 DDL); 1 is borderline (#25099: a platform object's declared option set — `narrowed` if a platform object's field options count as the protocol an author writes against). No other PR was edited. - **H5** (out of claim): the spellings that must learn the new line are `.claude/skills/pm-dispatch/SKILL.md:314` (`Clause-②: yes | no` in the claim template), `.claude/agents/os-dev.md:277` and `:280`, `.claude/skills/pm-dispatch/references/execution-duties.md:70`, `contract-review.md:8–19`, `landing-operations.md:11` — listed in the report with `carrier: seat`. Until they do, dispatched PRs will meet `not-measured-material` and clear it by one body line. ## Verification Final head `99eaf8350`; every exit code captured before any pipe (`cmd > log 2>&1; EXIT=$?`). - Self-tests of the edited scripts: `check-changeset-no-major --self-test` 455 assertions (the first run after the gate change failed 77 fixtures that pinned the old `yes ⇒ minor` coupling — the fixtures were updated, none deleted; every battery stays at or above its floor); `check-adr-0087-registration --self-test` 548; `check-empty-changeset --self-test` 170 (the byte-identical entry-regex pin across the four readers still holds); `objectui-changeset-digest --self-test` all checks (end-to-end through `bump-objectui.sh` and the real ADR-0087 gate); `release-github-releases --self-test` 103; `pnpm check:pm-dispatch-gates` 1913/1913 (fast tier). - Real scans on this branch against `origin/main`: `check-adr-0087-registration --base origin/main` exit 0 (the convention-rot fixtures run on every real scan, `narrowed` among them); `check-changeset-no-major --base origin/main` exit 0 (locally the level axis takes its `no-pull-request` lane, by design). - `eslint --no-inline-config` on the five edited scripts: exit 0. - Derived union, `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` off the merge base (8 paths): 64 commands, all run locally, all exit 0 at the final head. Two needed a second pass and neither was a finding: `pnpm --filter @objectstack/lint run check:doc-formula-expressions` exited 3 (PREREQUISITE NOT MET, `@objectstack/formula`/`lint` unbuilt in a fresh worktree) and exits 0 after `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under the verify lock; `pnpm check:pm-dispatch-gates` failed 1 of 1913 on the `no-path-population` marker `check:release-body` used to carry, which the new `pm/clause2-line.mjs` import contradicts by the harvester's own rule — the marker is dropped (the reader IS that family's one path population), 1913/1913 after. `--ran` reconciliation with recorded exit codes: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN. - CI-owned, not run here: `pnpm lint` (repo-wide), `test:repo`, `Test Core`; the diff touches no `packages/**` path, so the package build closure and package tests are n/a, as are `check:dev-prereqs` and `check-platform-checklist --changed-from` (no exported symbol renamed or deleted — `not-declared` was a verdict string, not an export). No ablation: the diff is gates, a workflow and docs, whose pins are the self-tests above, driven red → green by the change. - Dogfood battery: n/a — no `!` in the title and no `(narrowing)` arm. - Changeset: `skip-changeset` — nothing published moves (every changed path is `scripts/**`, `.github/**`, `docs/**` or `AGENTS.md`; no package's `files[]` reaches any of them). ## Acceptance notes - Residual, named rather than silent: the baseline leg sees a changed key set and a changed default; a refused value, a narrowed enum or a changed meaning is invisible to it, so the declaration (not the diff) stays the carrier — stated in the gate's header and in "WHICH LEVEL". - The kind is read from the PR body for the gate and from the changeset body for the ADR-0087 gate and the release notes — the same two-carrier shape the `Clause-②:` line already has; nothing cross-checks the two copies, as nothing did before. - `inPreMode` in the digest keeps its export (the RC exemption no longer reaches the console grade; the function has no other caller in `scripts/`), noted so a later reader does not take its presence for a live exemption. - Not filed, observation only: the release lane's `annotate-shipped` writes a "Shipped in" lead line above the changeset's declarations; the release-body splitter reads the whole bullet so the lines are still found. ## 维护者速读(草稿) **改了什么。** 按您 2026-10-21 的裁决 A 落地:版本号只跟元数据协议走。每个 PR 和 changeset 在 `Clause-②:` 旁多写一行 `Metadata-protocol: added | narrowed | semantic | none`;门禁 `check-changeset-no-major` 据此判级 —— 协议加了/收窄了/语义变了,至少一个被改包 `minor`;`none` 一律 `patch`,带 `minor` 即拒,改了 `authorable-surface/`、`authorable-defaults/` 基线却声明 `none` 也拒;`added` 配 `Clause-②: no`、`narrowed` 不带 `(narrowing)` 臂是自相矛盾,拒。ADR-0087 门禁把 `narrowed` 当破坏性信号;console 刷新 changeset 固定 `patch`;GitHub Release 正文按「需手动处理 / 自动迁移 / 新增能力 / 语义变化 / 其他修复」五段重排(CHANGELOG 不动)。AGENTS.md 与 pr-automation.yml 的规则文字同步改写。 **为什么改。** 现状是几乎每次发版都是 minor:95 个待发 changeset 里约一半是 minor,其中只有十来个真的动了元数据协议,版本号说不出协议动没动。裁决 A 把 minor 留给协议变化,`added` 之所以也是 minor,是因为 spec schema 不是 `.strict()`,旧运行时会静默丢掉不认识的键,写新键的 app 要能指一个 minor 底线。 **风险与代价(含回滚)。** ① 过渡成本:现开的 12 个 PR 里 10 个带 changeset,落地当天都会在该门禁上读到「未声明」而红,每个 PR 在正文加一行即清,不用推送;其中按我的判读 4 个是非协议 minor,需按新规改成 patch(报告里逐个点名,由席位和作者定)。② `.claude/**` 的认领模板与 os-dev 不在本卡范围,未改;改之前新派发的 PR 都会先红一次再补行。③ 基线腿只看得见键和默认值,拒值、收窄枚举、语义变化仍靠声明。回滚:整 PR revert 即可,无数据迁移、无发布面变化(`skip-changeset`)。 **席位意见。** **你要做的。** Tier H 人合:看一眼 AGENTS.md 第 1084–1101 行与 pr-automation.yml 的 "WHICH LEVEL" 两段规则文字是否就是您要的表述;认可则批准,席位入队落地。另请定 #25099 那类「平台对象字段选项集收窄」算不算元数据协议 `narrowed`(我的建议:算,因为它改的是作者可写的值域)。 --- _Generated by [Claude Code](https://claude.ai/code/session_01VsqhiSrBjB1wJfRNcHPeXN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ae58b26 commit f2b6cf2

8 files changed

Lines changed: 1347 additions & 295 deletions

‎.github/workflows/pr-automation.yml‎

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -751,46 +751,68 @@ jobs:
751751
1. It releases something
752752
-> run 'pnpm changeset' and name the packages it releases.
753753
754-
WHICH LEVEL: A purely additive widening of a published package's
755-
public surface (a new exported symbol on an `index`, a new accepted
756-
key or value) takes at least `minor`. The commit type may raise a
757-
bump but never lower it below what the act requires; a `fix(` that
758-
widens an index is therefore `minor`, and a `fix(` that changes no
759-
public surface stays `patch`. During the launch window `major` stays
760-
refused by `check-changeset-no-major` and breaking-ness is carried by
761-
the BREAKING banner plus the ADR-0087 disposition, not by the level.
762-
Ruled by the maintainer on 2026-09-04 (decision batch #35) on #15294,
763-
which recorded two contract reviews reading the repo's own history to
764-
OPPOSITE bumps for the same additive act. The commit type is not the
765-
discriminator it looked like: it correlates with the act, and when
766-
they disagree the act wins. The 64 historical `patch` precedents are
767-
pre-rule and nothing is retro-fixed.
754+
WHICH LEVEL: the level follows the METADATA PROTOCOL alone -- the
755+
change to what a metadata author can write -- and the commit type
756+
never decides it (maintainer ruling A, 2026-10-21, on #25105; it
757+
amends the 2026-09-04 rule of decision batch #35 on #15294, under
758+
which nearly every release was a minor: of 95 pending changesets
759+
about half were `minor` and about a dozen moved the protocol).
760+
Declare the kind on a line of its own, in the PR body AND in the
761+
changeset body, beside the `Clause-②:` line:
762+
763+
Metadata-protocol: added a new authorable key, a new accepted
764+
value or enum member, a new metadata
765+
type -> `minor`
766+
Metadata-protocol: narrowed a key removed or tombstoned, a value
767+
refused, a requirement tightened
768+
(declare the `(narrowing)` arm too)
769+
-> `minor`
770+
Metadata-protocol: semantic the meaning or default of an
771+
authorable key changes -> `minor`
772+
Metadata-protocol: none everything else: description or
773+
error text, non-authorable internal
774+
types, REST, drivers, services, the
775+
console, `feat:` and `fix!:` alike
776+
-> `patch`
777+
778+
`added` takes `minor` because spec schemas are not `.strict()`: an
779+
older runtime silently strips a key it does not know, so an app
780+
that writes a new key must be able to name a minor floor. Where
781+
several kinds apply, name the one highest in breaking-ness:
782+
`narrowed` over `semantic` over `added`. A non-protocol breaking
783+
change ships `patch` and KEEPS its carriers: the BREAKING banner
784+
and the ADR-0087 disposition are unchanged. `major` stays refused
785+
by `check-changeset-no-major` during the launch window. The
786+
GitHub Release body is grouped from these declarations by what the
787+
reader must do: 需手动处理 / 自动迁移 / 新增能力 / 语义变化 / 其他修复.
768788
769789
PART OF THIS IS A GATE, and which part is the difference between
770790
grading well and being refused. `check-changeset-no-major.mjs`
771-
refuses `major` and says why in its header. Since #16055 the SAME
772-
script also runs a LEVEL AXIS over the rule above, and that
773-
script's header states its scope: "a PR that DECLARES clause ② (a
774-
new key on a published payload) must grade AT LEAST ONE package
775-
whose published source it moves `minor` or above". It mechanizes
776-
this ruling "for exactly the PRs where the widening is already
777-
DECLARED, and for no others", and its `judgeLevel` verdict table
778-
spells both directions: `not-declared`, "the declaration reads
779-
`no`", exits 0; `enforce`, "declared `yes`, moved packages graded
780-
`patch` and NONE of them graded `minor` or above", exits 1. So on
781-
a PR that declares clause ②, grading `patch` across the board is
782-
refused; on every other PR the axis stands down and the two levels
783-
below `major` are yours, as they always were.
791+
refuses `major` and says why in its header. The SAME script runs
792+
the LEVEL AXIS over the rule above, reading both lines from the PR
793+
body on every `opened`, `synchronize` and `edited` event, and its
794+
`judgeLevel` verdict table spells every direction: a declared kind
795+
must grade at least ONE package whose published source the diff
796+
moves `minor` or above (`enforce`, exit 1 otherwise); a declared
797+
`none` must grade every entry it introduces `patch` (`none-minor`,
798+
exit 1) and may not move a `packages/spec/authorable-surface/` or
799+
`authorable-defaults/` shard (`none-baseline-moved`, exit 1);
800+
`added` beside `Clause-②: no`, or `narrowed` beside a line without
801+
the `(narrowing)` arm, is a `contradiction` (exit 1); and a PR that
802+
introduces a changeset entry with no readable `Metadata-protocol:`
803+
line is refused as NOT MEASURED (exit 1) rather than ticked -- the
804+
body is re-read on `edited`, so that red clears with no push.
784805
785806
WHAT THE AXIS DOES NOT JUDGE, in its own words: "WHICH of the
786807
packages above received that widening is NOT something this gate
787-
can read, and it does not claim to. Clause ② is declared ONCE, FOR
788-
THE PR" -- the declaration line in the PR body names no package.
789-
The axis cross-checks a declaration
790-
against the levels; it never reads the diff to find the act.
791-
Judging the act is still yours, this paragraph is still where the
792-
rule lives, and everything in it the axis does not reach is still
793-
prose.
808+
can read, and it does not claim to. The kind is declared ONCE, FOR
809+
THE PR" -- the declaration lines in the PR body name no package. A
810+
refused value, a narrowed enum or a changed meaning is invisible to
811+
the baselines, so the declaration, not the diff, is the carrier.
812+
The axis cross-checks the declarations against the levels; it never
813+
reads the diff to find the act. Judging the act is still yours,
814+
this paragraph is still where the rule lives, and everything in it
815+
the axis does not reach is still prose.
794816
795817
2. It releases nothing (.github/, .claude/, docs/, content/,
796818
examples/, tests-only, and the like)

‎AGENTS.md‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,24 +1081,24 @@ Both non-handshake shapes, and how to classify and probe your own:
10811081
surface** (Prime Directive #14 names them — more than ADRs): push it, open the PR, and stop
10821082
there; landing waits for its tier's record — the maintainer's word, or a seat's contract-tier
10831083
review. For that class, a finished task = a PR left visibly awaiting that record.
1084-
3. **Add a changeset for anything that publishes.** Feature, functional improvement or fix — run `pnpm changeset`
1085-
(or add a `.changeset/*.md` entry) describing it before committing. A bug fix in a released package takes a
1086-
**`patch`** changeset — never none, and ⛔ never `skip-changeset`: that label is for a diff that publishes
1087-
nothing from any released package. The declaration is `Clause-②: yes|no` plus at most one arm from the closed pair
1088-
`(widening)`/`(narrowing)`: `yes` takes at least **`minor`**, `(narrowing)` is BREAKING, `no (widening)` malformed.
1089-
**Breaking changesets must carry their migration.** If the change removes or renames anything an author can write (a
1090-
spec key, an export, a config field), the changeset body must state the FROM → TO mapping and the one-line fix —
1091-
this text ships to consumers as `CHANGELOG.md` inside the npm package and is what an upgrading agent greps after the
1092-
tombstone error. Removing an authorable spec key also requires a tombstone so the rejection itself carries the
1093-
prescription — `retiredKey()` (`packages/spec/src/shared/retired-key.ts`) on the schema whether or not it is
1094-
`.strict()`, and an entry in the shape's `*_RETIRED_KEY_GUIDANCE` map (see `data/mapping.zod.ts`) only for a
1095-
spelling the shape never declared, such as the retired key's old alias, where a tombstone has no property to
1096-
replace. The changeset is one of fourteen surfaces a retirement touches — follow the
1097-
`spec-property-retirement` skill (`.claude/skills/`) rather than reconstructing the kit, and note that a tombstone
1098-
keeps its liveness-ledger row while a key deleted without one loses it.
1099-
**A breaking changeset must also state its ADR-0087 disposition, in writing** — exactly one marker in the changeset
1100-
body, which also carries the PR's `Clause-②` line: `pnpm check:adr-0087-registration` reads the arm there. ⛔ The
1101-
categories are NOT copied here — the gate prints the full set when it fails.
1084+
3. **Add a changeset for anything that publishes** (`pnpm changeset` before committing). A bug fix in a released
1085+
package takes a **`patch`** changeset — never none, and ⛔ never `skip-changeset`: that label is for a diff that
1086+
publishes nothing from any released package. Two line-initial declarations, in the PR body and the changeset body:
1087+
`Clause-②: yes|no` plus at most one arm from the closed pair `(widening)`/`(narrowing)` (`(narrowing)` is
1088+
BREAKING, `no (widening)` malformed), and `Metadata-protocol: added|narrowed|semantic|none`, the change to what a
1089+
metadata author can write — a new key, accepted value or metadata type is `added`; a key removed or tombstoned, a
1090+
value refused or a requirement tightened is `narrowed`; a changed meaning or default is `semantic`; all else is
1091+
`none`; several ⇒ `narrowed` over `semantic` over `added`. The level follows the kind alone: a kind takes at least
1092+
**`minor`** on a package the diff moves, `none` takes **`patch`**; a non-protocol change, widening or breaking,
1093+
ships `patch` and keeps its BREAKING banner and ADR-0087 disposition. **Breaking changesets must carry their
1094+
migration**: removing or renaming anything an author can write (a spec key, an export, a config field) puts the FROM
1095+
→ TO mapping and the one-line fix in the body, which ships as `CHANGELOG.md` in the npm package. Removing an
1096+
authorable spec key also needs a tombstone (`retiredKey()`, `packages/spec/src/shared/retired-key.ts`) so the
1097+
rejection carries the prescription — one of fourteen retirement surfaces; follow the `spec-property-retirement`
1098+
skill (`.claude/skills/`), never reconstruct the kit. **A breaking changeset must also state its ADR-0087
1099+
disposition, in writing** — exactly one marker in the changeset body, which also carries both declaration lines
1100+
(`pnpm check:adr-0087-registration` reads the arm and the kind there). ⛔ The categories are NOT copied here —
1101+
the gate prints the full set when it fails.
11021102
4. **A removal that breaks the pinned sibling checkout ships together with the sibling fix and the pin bump — or it
11031103
does not ship.** The full battery's `Console Pin Gate` builds objectui at the pinned `.objectui-sha` against
11041104
**current** `main`, so such a removal is a main-red the moment it merges; its ruling therefore authorizes the

‎docs/releases-maintenance.md‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,12 @@ repo and bundled into `@objectstack/console` at a pinned commit recorded in
3939
[`.objectui-sha`](../.objectui-sha). A SHA bump alone used to leave **no trace** in
4040
the history. `scripts/bump-objectui.sh` now closes that gap: on every bump it emits
4141
a `@objectstack/console` changeset summarising the objectui `OLD_SHA..NEW_SHA`
42-
commit range (auto-typed `minor` if the range contains any `feat`, else `patch`).
43-
So frontend deltas flow through the *same* pipeline as the backend — they land in
44-
`@objectstack/console`'s changelog and roll up into the platform version.
42+
commit range, graded `patch`: the platform's version line follows the metadata
43+
protocol, which the console never changes, so the file carries
44+
`Metadata-protocol: none` and `CONSOLE_BUMP` is the only thing that grades it
45+
otherwise. So frontend deltas flow through the *same* pipeline as the backend —
46+
they land in `@objectstack/console`'s changelog and roll up into the platform
47+
version.
4548

4649
```bash
4750
scripts/bump-objectui.sh # bump to ../objectui HEAD, write the changeset, commit
@@ -167,6 +170,25 @@ platform version maps to.
167170
page under a "What's new in N.x" running section, or leave them to the generated
168171
per-package changelogs.
169172

173+
### Release notes are grouped by what the reader must do
174+
175+
The GitHub Release body is not the raw CHANGELOG section. `scripts/release-github-releases.mjs`
176+
regroups that section's entries — byte for byte, none dropped — under five headings, in this
177+
order, each read from the declarations the changeset already carries (`Metadata-protocol:`, the
178+
`Clause-②:` arm, the BREAKING banner, the ADR-0087 disposition marker):
179+
180+
| Section | What places an entry there |
181+
|:--|:--|
182+
| 需手动处理 | breaking (`Metadata-protocol: narrowed`, a `(narrowing)` arm, a BREAKING banner or a `!` summary) with no `registered` ADR-0087 conversion |
183+
| 自动迁移 | breaking, with a `registered` conversion that `objectstack migrate meta` applies |
184+
| 新增能力 | `Metadata-protocol: added`, or a non-breaking `Clause-②: yes` widening |
185+
| 语义变化 | `Metadata-protocol: semantic` |
186+
| 其他修复 | everything else — `none`, dependency bumps, and entries written before the declaration existed |
187+
188+
A section with nothing in it is omitted. A curated `vN.mdx` section for the same release uses the
189+
same five headings in the same order, so the hand-written page and the generated body agree on
190+
what a reader must do; the per-package `CHANGELOG.md` keeps changesets' own level grouping.
191+
170192
### Sourcing the Console section for a release
171193

172194
Backend content comes from the spec/package changesets. Frontend content comes from

0 commit comments

Comments
 (0)