Skip to content

Commit 4897791

Browse files
committed
feat(ci): ADR-0087 台账完备性门禁 —— 声明为 breaking 的 changeset 必须写明台账处置 (#6148)
ADR-0087 台账原有两道门禁(check:spec-changes / check:upgrade-guide)钉的都是 「台账 ↔ 生成物同步」。生成物是 registry 的纯投影,所以当条目**从一开始就不存在** 时,两者互相一致 —— 全仓全绿。PR #6048 删除 ctx.user.roles 正是这个形状,靠人眼 比对才发现(#6011),补登记又另派了一轮(PR #6138)。 按维护者裁定,门禁以 changeset 自己的 breaking/major 声明为驱动,绕开「什么算一次 需要登记的退役」这个跨包不可判定问题:作者已经声明过「这是 breaking」,门禁只需 注意到它对台账**只字未提**。⛔ 不做跨包退役自动检测。 四种处置,三种可机械复验、每次运行都重新验证: registered <id> —— id 必须在台账中存在,且至少一个是本次 diff 新增的 not-required (unpublished) —— 所有 bump 的包必须 private: true not-required (already-registered) —— 命名的 id 必须在 merge base 就已存在 not-required (no-migration-prescription) —— 当 changeset 自己带 FROM → TO 迁移说明时**拒绝**: 一份发布了改写指令的 changeset 不能同时声称无人需要改写 实测(main 最近 400 个 first-parent commit):32 条 breaking changeset 中仅 5 条 (15.6%)动过台账,故 not-required 是常态而非例外 —— 门禁的价值不是判定谁该登记, 而是让这个问题**被书面回答**。#6048 没有回答,也没有任何东西问过它。 反向验证:在真实提交 dca5bd3(#6048)上门禁转红并指名 tidy-donkeys-yawn.md; 去掉「必须有 marker」这一步后同一输入转绿。三处 ablation 各自让 self-test 相应 红路径转绿,证明 self-test 不是空转。 #4690:所有输入都断言非空,另有两道防空转断言 —— 生成物中的 migrationId 必须 都能被源码解析器看见(解析器腐化),以及库存中至少有一条能被 breaking 检测器 命中(约定改写)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5
1 parent 2bc1876 commit 4897791

4 files changed

Lines changed: 1216 additions & 0 deletions

File tree

.github/workflows/pr-automation.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,44 @@ jobs:
447447
node scripts/check-empty-changeset.mjs --self-test
448448
node scripts/check-empty-changeset.mjs --base "$MERGE_BASE"
449449
450+
# #6148: a declared-breaking changeset must say IN WRITING what it did about
451+
# the ADR-0087 migration ledger. The two existing ledger gates
452+
# (`check:spec-changes`, `check:upgrade-guide`) both pin ledger <-> ARTIFACT
453+
# SYNCHRONY; neither pins that a retirement which actually happened has an
454+
# entry at all. The artifacts are a pure projection of the registry, so an
455+
# entry that was NEVER written leaves the two perfectly consistent and every
456+
# gate green, repo-wide. PR #6048 removed `ctx.user.roles` exactly that way;
457+
# a human triage seat caught it by eye (#6011), and a separate dispatch round
458+
# backfilled it (PR #6138).
459+
#
460+
# This step lives HERE, beside check-empty-changeset, rather than in ci.yml's
461+
# lint family, for one reason: its verdict is a function of the PR's DIFF, so
462+
# it needs the same `$MERGE_BASE` the steps above derive. A lint-job copy
463+
# would have no branch point to judge against and would fall back to reading
464+
# stock -- which is precisely the "judge the author for what main gained while
465+
# their PR was open" defect (#6129) the merge base exists to prevent.
466+
#
467+
# It does NOT detect retirements. Cross-package retirement detection is
468+
# undecidable in the case that actually happened (the removal was in
469+
# `packages/runtime`, the ledger is in `packages/spec`), and the maintainer's
470+
# ruling on #6148 routes around it: the AUTHOR has already declared "breaking"
471+
# in the changeset, so the gate only has to notice that a declared-breaking
472+
# change said nothing. Measured, ~1 declared-breaking change in 7 needs a
473+
# ledger entry, so `not-required (...)` is the ordinary answer and costs one
474+
# line -- but it is a line someone wrote and a reviewer can see.
475+
#
476+
# `--self-test` runs AHEAD of the real check, the repo convention for
477+
# `check-*.mjs` gates: its fixtures are the only place the red directions are
478+
# pinned, and each of them was verified to flip green when the corresponding
479+
# check is ablated. Real temp git repositories, well under a second.
480+
- name: Require an ADR-0087 disposition on a declared-breaking changeset
481+
if: steps.labels.outputs.skip != 'true'
482+
env:
483+
MERGE_BASE: ${{ steps.diffbase.outputs.merge_base }}
484+
run: |
485+
node scripts/check-adr-0087-registration.mjs --self-test
486+
node scripts/check-adr-0087-registration.mjs --base "$MERGE_BASE"
487+
450488
- name: Guard against accidental major bumps (launch window)
451489
# Every publishable package is in one Changesets "fixed" (lockstep) group,
452490
# so a single `major` bump promotes the ENTIRE monorepo to a new major

AGENTS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,14 @@ it to `OPEN_CAPABILITY_REGISTRIES` in the same PR that fixes it.
839839
isn't green yet). A finished task = a merged PR, not a dirty working tree.
840840
3. **Add a changeset for feature work.** When the change is a feature or functional improvement, run `pnpm changeset` (or add a `.changeset/*.md` entry) describing it before committing. Pure bug fixes do **not** require a changeset.
841841
**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 a non-strict schema, or an entry in the relevant `UNKNOWN_KEY_GUIDANCE` / `*_RETIRED_KEY_GUIDANCE` map (see `object.zod.ts`, `ai/tool.zod.ts`) when the schema is `.strict()`. 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 the two routes imply **opposite** liveness-ledger dispositions.
842+
**A breaking changeset must also state its ADR-0087 disposition, in writing.** Add exactly one marker to the changeset body — `pnpm check:adr-0087-registration` enforces it, and the CI step is *Require an ADR-0087 disposition on a declared-breaking changeset*:
843+
```
844+
<!-- adr-0087: registered SOME-MIGRATION-ID -->
845+
<!-- adr-0087: not-required (unpublished) why -->
846+
<!-- adr-0087: not-required (already-registered SOME-MIGRATION-ID) why -->
847+
<!-- adr-0087: not-required (no-migration-prescription) why -->
848+
```
849+
Why it is asked of you at all: the two ADR-0087 gates (`check:spec-changes`, `check:upgrade-guide`) pin ledger ↔ **artifact synchrony**, and the artifacts are a pure projection of the registry — so a retirement whose entry was **never written** leaves the two perfectly consistent and every gate in the repo green. PR #6048 removed `ctx.user.roles` that way and only a human comparing by eye caught it (#6011, backfilled by PR #6138). Ledger entries are the sole data source for `objectstack migrate meta`, `spec-changes.json` and the generated upgrade guide, and for a surface with **no spec schema** (`ctx.user` is only a runtime TS interface) there is no tombstone and no schema rejection either — the ledger entry is the *only* channel that reaches an upgrader. Measured: roughly **1 declared-breaking change in 7** needs an entry, so `not-required` is the ordinary answer and costs one line. Three of the four dispositions are re-verified mechanically on every run, and the fourth is refused when the changeset's own body carries a FROM → TO prescription — a changeset that ships migration instructions cannot also claim nobody must migrate (#6148).
842850
4. **Added or removed a `packages/spec` export? Run `pnpm --filter @objectstack/spec gen:api-surface` and commit the result.** The `TypeScript Type Check` job diffs spec's built export surface against `api-surface/` (one shard per entry point since #5837); a new export makes the snapshot stale and turns the job red. It reads the **built `dist` declarations**, so `OS_SKIP_DTS=1` — the flag you reach for to make local builds fast — skips exactly the artifact the gate inspects, and the check passes locally while failing in CI. Same shape for the other generated-artifact gates in that job (`check:docs`, `check:skill-refs`, `check:react-blocks`), which read `src/` and so do reproduce locally.
843851
5. Update `CHANGELOG.md` / `ROADMAP.md` if user-facing or architectural.
844852
6. **Delete temporary artifacts** — screenshots, traces, scratch logs, `.playwright-mcp/`, throwaway `tmp*.ts`, ad-hoc scripts. Repo must look identical to before, minus intended changes.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"check:objectui-pin-fresh": "node scripts/check-objectui-pin-fresh.mjs --self-test && node scripts/check-objectui-pin-fresh.mjs",
5858
"check:prerelease-pins": "node scripts/check-prerelease-pin-watch.mjs --self-test && node scripts/check-prerelease-pin-watch.mjs",
5959
"check:empty-changeset": "node scripts/check-empty-changeset.mjs --self-test && node scripts/check-empty-changeset.mjs",
60+
"check:adr-0087-registration": "node scripts/check-adr-0087-registration.mjs --self-test && node scripts/check-adr-0087-registration.mjs",
6061
"check:override-consistency": "node scripts/check-override-consistency.mjs --self-test && node scripts/check-override-consistency.mjs",
6162
"check:release-notes": "node scripts/check-release-notes.mjs",
6263
"check:release-body": "node scripts/release-github-releases.mjs --self-test",

0 commit comments

Comments
 (0)