Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 56 additions & 34 deletions .github/workflows/pr-automation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -751,46 +751,68 @@ jobs:
1. It releases something
-> run 'pnpm changeset' and name the packages it releases.

WHICH LEVEL: A purely additive widening of a published package's
public surface (a new exported symbol on an `index`, a new accepted
key or value) takes at least `minor`. The commit type may raise a
bump but never lower it below what the act requires; a `fix(` that
widens an index is therefore `minor`, and a `fix(` that changes no
public surface stays `patch`. During the launch window `major` stays
refused by `check-changeset-no-major` and breaking-ness is carried by
the BREAKING banner plus the ADR-0087 disposition, not by the level.
Ruled by the maintainer on 2026-09-04 (decision batch #35) on #15294,
which recorded two contract reviews reading the repo's own history to
OPPOSITE bumps for the same additive act. The commit type is not the
discriminator it looked like: it correlates with the act, and when
they disagree the act wins. The 64 historical `patch` precedents are
pre-rule and nothing is retro-fixed.
WHICH LEVEL: the level follows the METADATA PROTOCOL alone -- the
change to what a metadata author can write -- and the commit type
never decides it (maintainer ruling A, 2026-10-21, on #25105; it
amends the 2026-09-04 rule of decision batch #35 on #15294, under
which nearly every release was a minor: of 95 pending changesets
about half were `minor` and about a dozen moved the protocol).
Declare the kind on a line of its own, in the PR body AND in the
changeset body, beside the `Clause-②:` line:

Metadata-protocol: added a new authorable key, a new accepted
value or enum member, a new metadata
type -> `minor`
Metadata-protocol: narrowed a key removed or tombstoned, a value
refused, a requirement tightened
(declare the `(narrowing)` arm too)
-> `minor`
Metadata-protocol: semantic the meaning or default of an
authorable key changes -> `minor`
Metadata-protocol: none everything else: description or
error text, non-authorable internal
types, REST, drivers, services, the
console, `feat:` and `fix!:` alike
-> `patch`

`added` takes `minor` because spec schemas are not `.strict()`: an
older runtime silently strips a key it does not know, so an app
that writes a new key must be able to name a minor floor. Where
several kinds apply, name the one highest in breaking-ness:
`narrowed` over `semantic` over `added`. A non-protocol breaking
change ships `patch` and KEEPS its carriers: the BREAKING banner
and the ADR-0087 disposition are unchanged. `major` stays refused
by `check-changeset-no-major` during the launch window. The
GitHub Release body is grouped from these declarations by what the
reader must do: 需手动处理 / 自动迁移 / 新增能力 / 语义变化 / 其他修复.

PART OF THIS IS A GATE, and which part is the difference between
grading well and being refused. `check-changeset-no-major.mjs`
refuses `major` and says why in its header. Since #16055 the SAME
script also runs a LEVEL AXIS over the rule above, and that
script's header states its scope: "a PR that DECLARES clause ② (a
new key on a published payload) must grade AT LEAST ONE package
whose published source it moves `minor` or above". It mechanizes
this ruling "for exactly the PRs where the widening is already
DECLARED, and for no others", and its `judgeLevel` verdict table
spells both directions: `not-declared`, "the declaration reads
`no`", exits 0; `enforce`, "declared `yes`, moved packages graded
`patch` and NONE of them graded `minor` or above", exits 1. So on
a PR that declares clause ②, grading `patch` across the board is
refused; on every other PR the axis stands down and the two levels
below `major` are yours, as they always were.
refuses `major` and says why in its header. The SAME script runs
the LEVEL AXIS over the rule above, reading both lines from the PR
body on every `opened`, `synchronize` and `edited` event, and its
`judgeLevel` verdict table spells every direction: a declared kind
must grade at least ONE package whose published source the diff
moves `minor` or above (`enforce`, exit 1 otherwise); a declared
`none` must grade every entry it introduces `patch` (`none-minor`,
exit 1) and may not move a `packages/spec/authorable-surface/` or
`authorable-defaults/` shard (`none-baseline-moved`, exit 1);
`added` beside `Clause-②: no`, or `narrowed` beside a line without
the `(narrowing)` arm, is a `contradiction` (exit 1); and a PR that
introduces a changeset entry with no readable `Metadata-protocol:`
line is refused as NOT MEASURED (exit 1) rather than ticked -- the
body is re-read on `edited`, so that red clears with no push.

WHAT THE AXIS DOES NOT JUDGE, in its own words: "WHICH of the
packages above received that widening is NOT something this gate
can read, and it does not claim to. Clause ② is declared ONCE, FOR
THE PR" -- the declaration line in the PR body names no package.
The axis cross-checks a declaration
against the levels; it never reads the diff to find the act.
Judging the act is still yours, this paragraph is still where the
rule lives, and everything in it the axis does not reach is still
prose.
can read, and it does not claim to. The kind is declared ONCE, FOR
THE PR" -- the declaration lines in the PR body name no package. A
refused value, a narrowed enum or a changed meaning is invisible to
the baselines, so the declaration, not the diff, is the carrier.
The axis cross-checks the declarations against the levels; it never
reads the diff to find the act. Judging the act is still yours,
this paragraph is still where the rule lives, and everything in it
the axis does not reach is still prose.

2. It releases nothing (.github/, .claude/, docs/, content/,
examples/, tests-only, and the like)
Expand Down
36 changes: 18 additions & 18 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1081,24 +1081,24 @@ Both non-handshake shapes, and how to classify and probe your own:
surface** (Prime Directive #14 names them — more than ADRs): push it, open the PR, and stop
there; landing waits for its tier's record — the maintainer's word, or a seat's contract-tier
review. For that class, a finished task = a PR left visibly awaiting that record.
3. **Add a changeset for anything that publishes.** Feature, functional improvement or fix — run `pnpm changeset`
(or add a `.changeset/*.md` entry) describing it before committing. A bug fix in a released package takes a
**`patch`** changeset — never none, and ⛔ never `skip-changeset`: that label is for a diff that publishes
nothing from any released package. The declaration is `Clause-②: yes|no` plus at most one arm from the closed pair
`(widening)`/`(narrowing)`: `yes` takes at least **`minor`**, `(narrowing)` is BREAKING, `no (widening)` malformed.
**Breaking changesets must carry their migration.** If the change removes or renames anything an author can write (a
spec key, an export, a config field), the changeset body must state the FROM → TO mapping and the one-line fix —
this text ships to consumers as `CHANGELOG.md` inside the npm package and is what an upgrading agent greps after the
tombstone error. Removing an authorable spec key also requires a tombstone so the rejection itself carries the
prescription — `retiredKey()` (`packages/spec/src/shared/retired-key.ts`) on the schema whether or not it is
`.strict()`, and an entry in the shape's `*_RETIRED_KEY_GUIDANCE` map (see `data/mapping.zod.ts`) only for a
spelling the shape never declared, such as the retired key's old alias, where a tombstone has no property to
replace. The changeset is one of fourteen surfaces a retirement touches — follow the
`spec-property-retirement` skill (`.claude/skills/`) rather than reconstructing the kit, and note that a tombstone
keeps its liveness-ledger row while a key deleted without one loses it.
**A breaking changeset must also state its ADR-0087 disposition, in writing** — exactly one marker in the changeset
body, which also carries the PR's `Clause-②` line: `pnpm check:adr-0087-registration` reads the arm there. ⛔ The
categories are NOT copied here — the gate prints the full set when it fails.
3. **Add a changeset for anything that publishes** (`pnpm changeset` before committing). A bug fix in a released
package takes a **`patch`** changeset — never none, and ⛔ never `skip-changeset`: that label is for a diff that
publishes nothing from any released package. Two line-initial declarations, in the PR body and the changeset body:
`Clause-②: yes|no` plus at most one arm from the closed pair `(widening)`/`(narrowing)` (`(narrowing)` is
BREAKING, `no (widening)` malformed), and `Metadata-protocol: added|narrowed|semantic|none`, the change to what a
metadata author can write — a new key, accepted value or metadata type is `added`; a key removed or tombstoned, a
value refused or a requirement tightened is `narrowed`; a changed meaning or default is `semantic`; all else is
`none`; several ⇒ `narrowed` over `semantic` over `added`. The level follows the kind alone: a kind takes at least
**`minor`** on a package the diff moves, `none` takes **`patch`**; a non-protocol change, widening or breaking,
ships `patch` and keeps its BREAKING banner and ADR-0087 disposition. **Breaking changesets must carry their
migration**: removing or renaming anything an author can write (a spec key, an export, a config field) puts the FROM
→ TO mapping and the one-line fix in the body, which ships as `CHANGELOG.md` in the npm package. Removing an
authorable spec key also needs a tombstone (`retiredKey()`, `packages/spec/src/shared/retired-key.ts`) so the
rejection carries the prescription — one of fourteen retirement surfaces; follow the `spec-property-retirement`
skill (`.claude/skills/`), never reconstruct the kit. **A breaking changeset must also state its ADR-0087
disposition, in writing** — exactly one marker in the changeset body, which also carries both declaration lines
(`pnpm check:adr-0087-registration` reads the arm and the kind there). ⛔ The categories are NOT copied here —
the gate prints the full set when it fails.
4. **A removal that breaks the pinned sibling checkout ships together with the sibling fix and the pin bump — or it
does not ship.** The full battery's `Console Pin Gate` builds objectui at the pinned `.objectui-sha` against
**current** `main`, so such a removal is a main-red the moment it merges; its ruling therefore authorizes the
Expand Down
28 changes: 25 additions & 3 deletions docs/releases-maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ repo and bundled into `@objectstack/console` at a pinned commit recorded in
[`.objectui-sha`](../.objectui-sha). A SHA bump alone used to leave **no trace** in
the history. `scripts/bump-objectui.sh` now closes that gap: on every bump it emits
a `@objectstack/console` changeset summarising the objectui `OLD_SHA..NEW_SHA`
commit range (auto-typed `minor` if the range contains any `feat`, else `patch`).
So frontend deltas flow through the *same* pipeline as the backend — they land in
`@objectstack/console`'s changelog and roll up into the platform version.
commit range, graded `patch`: the platform's version line follows the metadata
protocol, which the console never changes, so the file carries
`Metadata-protocol: none` and `CONSOLE_BUMP` is the only thing that grades it
otherwise. So frontend deltas flow through the *same* pipeline as the backend —
they land in `@objectstack/console`'s changelog and roll up into the platform
version.

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

### Release notes are grouped by what the reader must do

The GitHub Release body is not the raw CHANGELOG section. `scripts/release-github-releases.mjs`
regroups that section's entries — byte for byte, none dropped — under five headings, in this
order, each read from the declarations the changeset already carries (`Metadata-protocol:`, the
`Clause-②:` arm, the BREAKING banner, the ADR-0087 disposition marker):

| Section | What places an entry there |
|:--|:--|
| 需手动处理 | breaking (`Metadata-protocol: narrowed`, a `(narrowing)` arm, a BREAKING banner or a `!` summary) with no `registered` ADR-0087 conversion |
| 自动迁移 | breaking, with a `registered` conversion that `objectstack migrate meta` applies |
| 新增能力 | `Metadata-protocol: added`, or a non-breaking `Clause-②: yes` widening |
| 语义变化 | `Metadata-protocol: semantic` |
| 其他修复 | everything else — `none`, dependency bumps, and entries written before the declaration existed |

A section with nothing in it is omitted. A curated `vN.mdx` section for the same release uses the
same five headings in the same order, so the hand-written page and the generated body agree on
what a reader must do; the per-package `CHANGELOG.md` keeps changesets' own level grouping.

### Sourcing the Console section for a release

Backend content comes from the spec/package changesets. Frontend content comes from
Expand Down
Loading
Loading