Repository navigation
docs(skills): retirement kit routes a RETIRED_KEYS_BY_MAJOR entry through entries/retired-keys/ and gen:migration-registry, and an enum-value narrowing through enumWithRetiredValues() - #20828
Conversation
…hrough entries/retired-keys/ and gen:migration-registry Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg Co-authored-by: Claude <noreply@anthropic.com>
…mWithRetiredValues() The enum-value paragraph prescribed a hand-rolled `error` map dispatched on `issue.input`, citing HookBodyCapability. The generic helper in shared/retired-key.ts is the route now; its docblock names both hand-rolled precursors. Paid inside the two items: the kit stays at 337 lines. Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Read against: PR #20828 head Check-runs on the head ( ① Derived judgmentsText 1: §3, the
Text 3: §2, the enum-value paragraph (head L141-143).
Pins. Kit line count 337 at ② Semver levelNone: the diff publishes nothing. One file, ③ Boundary flags
Landing. Tier S: with this PASS on head Implemented-by: VERDICT: PASS |
…n-registry, never the generated registry.ts (objectstack-ai#21021) Fixes objectstack-ai#20809 Clause-②: no ## What this changes `RETIRED_KEYS_BY_MAJOR`, `RETIRED_DEFS_BY_MAJOR` and every step's `semantic` list are generated into `packages/spec/src/migrations/registry.ts` from one file per entry under `packages/spec/src/migrations/entries/`. Gate (b) of `packages/spec/scripts/build-schemas.ts`, the def-removal gate and four sibling remedies still told an author to paste lines into, or delete lines from, that generated file. An author who followed them typed between generated markers, which the next `gen:migration-registry` run reverts and `check:migration-registry` fails first. Every such remedy now names the entry file and the generator command. Where the gate knows the id and the major, it prints the exact file and the one declaration it carries. Where the major is the author's to establish (check (c) proof 1), it names the directory and `entries/README.md`. This is the route the retirement kit's checklist landed first (PR objectstack-ai#20828, merged as `745ac30a`), quoted from `.claude/skills/spec-property-retirement/SKILL.md` on `main`: > 在 `packages/spec/src/migrations/entries/retired-keys/` 加一个文件(命名见 `entries/README.md`),再跑 `pnpm --filter @objectstack/spec gen:migration-registry` 把它生成进 `registry.ts`,⛔ 永不在那里手打。 Gate (b) before, as printed at `05be35259`: ```text 1. Declare each retirement by its EXACT key in RETIRED_KEYS_BY_MAJOR (packages/spec/src/migrations/registry.ts) — copy these lines in: 'api/HttpFindQueryParams:distinct', under `17: [ … ]` (create the major's array if it is the first). ``` After (the template rendered for the test's fixture key; the pins below assert it from a real run): ```text 1. Register each retirement by its EXACT key in RETIRED_KEYS_BY_MAJOR. That table is generated: add one entry file per key (named per packages/spec/src/migrations/entries/README.md; these names are the generator's own) — packages/spec/src/migrations/entries/retired-keys/17.api__HttpFindQueryParams__distinct.ts export const entry = 'api/HttpFindQueryParams:distinct'; — with a `//` comment above the `export` saying why the key was retired (it lands above the entry in the table), then run `pnpm --filter @objectstack/spec gen:migration-registry`. ⛔ Never type an entry into the generated table by hand: the next run reverts it, and check:migration-registry fails it first. ... ``` The two docblocks the card names now say what the `guidance` slot is for: prescriptions for keys the shape does not declare (wrong-layer pointers, and the upgrade for a spelling removed from the shape). A key retired in place is a `retiredKey()` tombstone in the shape, never a guidance row, because `shared/alias-integrity.test.ts` ("no guidance key is itself a declared key") makes a guidance row for a declared key dead. No guidance table entry changes. ## The dispatch's hypotheses, measured **H1 — every printed sentence that routes an author into `migrations/registry.ts`.** `git grep -n -E 'migrations/registry\.ts|copy these lines|step in src/migrations' -- packages/spec/scripts/build-schemas.ts` returns 10 lines at `05be35259` and 2 at this head (both code comments). Positive control: the PM's four named sites (`:789`, `:1128`, `:2637`, `:2836`) are among the 10. I also read the lines that name a table without a file. Verdicts (line numbers at `05be35259`): | Site | Printed | Verdict | |---|---|---| | `:789` manifest disappearance | "declare each one in RETIRED_DEFS_BY_MAJOR (src/migrations/registry.ts)" | Same route. It now prints one `entries/retired-defs/` file per def, plus the generator. | | `:1089` check (a), aged-out route | "requires the exact key be declared in RETIRED_KEYS_BY_MAJOR" | Correct as it stands. It states a precondition, names no file and asks for no edit. | | `:1100` | code comment, where the gate READS the table | Correct. Not printed. | | `:1127-1130` gate (b) | "copy these lines in" | Fixed (this card). | | `:1183-1185` (b2) | "delete the entry from packages/spec/src/migrations/registry.ts" | Same route. Each row now names its entry file (from the entry's own id and major, so a key carried through a declared def rename still names the file on disk), plus the generator. | | `:1222-1224` (b3) | same | Same route, same fix. It also says that a respelled id renames the file. | | `:2578-2579` def (b2) mirror | same | Same route, same fix. | | `:2619` | info line "RETIRED_DEFS_BY_MAJOR, major N" | Correct. A report, not a remedy. | | `:2636-2639` def-removal gate | "copy these lines in" | Same route (pointer `5908348030`). Fixed like gate (b). | | `:2781` check (c) violation row | "no entry in RETIRED_KEYS_BY_MAJOR names this EXACT key" | Correct. A diagnosis; the route is in the footer's proof 1. | | `:2824-2825` check (c) proof 1 | "(src/migrations/registry.ts) … writes it down" | Same route. It now names the entry directory, `entries/README.md` and the generator. It names the README rather than a file because the true major is the author's to establish. | | `:2835-2836` check (c) proof 3 | "(src/migrations/registry.ts)" | Same route. It now names the entry directory; the manifest gate it defers to prints the file. | | `:3261-3262` default-change remedy | "add a `semantic` entry to this major's step in src/migrations/registry.ts" | Same defect on the third generated table. Fixed in place under the bounded rule: same defect class, same file, mechanical, same gate family. | **H2 — the naming source.** `entries/README.md` defines a file as `MAJOR.ID.ts` with `/` and `:` replaced by `__`. The generator's `shardNameFor` is, in its own words, "the whole naming rule, in one place", and `loadEntries` refuses a file whose name does not match its id. Every remedy prints its file name through `shardNameFor` (with `KINDS` / `ENTRIES_DIR` for the directory), imported from the generator. No second copy of the convention exists, and the README stays the one human description. **H3 — whether the docblock text ships.** Measured after `pnpm --filter @objectstack/spec build`, with `npm pack --dry-run`: - `src/ai/tool.zod.ts` ships verbatim through `files[]` (`src/**/*.zod.ts`). - `src/shared/strict-object.ts` does not ship as source, but its edited JSDoc is in `dist/shared/index.d.ts` and `dist/shared/index.d.mts`. Positive control: the untouched `aliases` docblock is in the same two files. - Neither script ships. So this PR takes a `patch` changeset, `.changeset/20809-guidance-slot-docblocks.md`, and no `skip-changeset`. ## Landing site beyond the claimed surface: the generator `build-migration-registry.ts` ran its CLI at module top level, and in write mode it rewrites `registry.ts`, so nothing could import `shardNameFor` from it. The fix is at the producer: the CLI body moves into `main()`, behind an entry guard spelled the way `check-generated.ts` (same directory) spells it, with realpath on both sides. `shardNameFor`'s docblock now names its second reader. Why not move the rule into a new `scripts/lib/` module, or import `scripts/invoked-as.mjs`: CI runs `check:migration-registry` only when a changed path is in its selector arm (`scripts/ci/select-gate-families.sh`, `migration_registry`). That arm lists the generator's read set path by path. A new import would add an input outside the arm, and a later edit to that input alone would skip the gate. The guard adds only `node:fs`'s `realpathSync`, so the generator's read set is unchanged. New test `packages/spec/scripts/build-migration-registry-entry.test.ts` pins both directions of the guard, each as a real `tsx` process: a direct run reaches the generator, a run through a symlink also reaches it, and an import prints nothing beyond what the importer asked for. ## Tests (at `c2e5b3b81`) - `pnpm --filter @objectstack/spec exec vitest run --project repo scripts/build-schemas-check-mode.test.ts`, the whole file: **88 passed (88)**, Duration 995 s, lock verdict `command-exit 0`. - Gate (b): the remedy names `entries/retired-keys/` plus `shardNameFor(major, key)`, the declaration and the generator command. It contains no paste line, no `copy these lines` and no `migrations/registry.ts`. - New control: every file and declaration pair is parsed off the red run's own output and written into the sandbox. The sandbox's own generator (which refuses a misnamed file) then runs, and gate (b) is silent on the re-run. The existing "registered by exact name ⇒ (b) silent" case still passes. - The def-removal gate, the manifest disappearance check, (b2), def (b2), (b3) and the end-to-end unpublish repro each pin their entry file and the generator. - `pnpm --filter @objectstack/spec test` (the `local` project, which includes the new guard test): **585 files, 17208 passed | 1 todo**, `command-exit 0`. - `pnpm --filter @objectstack/spec typecheck` (`tsc` on src, `tsconfig.scripts.json` covering both changed scripts and the new test, and the test-typecheck ledger): `command-exit 0`. - `pnpm --filter @objectstack/spec check:migration-registry`: `--self-test: ok`, `src/migrations/registry.ts is current (330 semantic, 242 retired-key, 211 retired-def)`. This is byte-identical to the reading taken before any edit. ## Ablations (one-shot; each leg went through `scripts/ablation-replace.mjs`, which proves the mutation landed and the restore matched HEAD) - **A1 — guard deleted** (`main()` called unconditionally). The import probe goes red: the importer printed `✓ wrote src/migrations/registry.ts (330 semantic, 242 retired-key, 211 retired-def)`. Both run probes stay green. The first A1 attempt was refused by the tool before any run, because the replacement was a substring of the anchor and its count could not rise. It is recorded here as a no-op, and the re-run used a distinct marker. - **A3 — raw `argv[1]` comparison** (no realpath). The symlink run probe goes red: exit 0 with empty output, the silent no-op the guard spelling exists to prevent. The other two stay green. - **B — gate (b) reverted to paste lines.** Of the six cases in gate (b)'s describe block, two go red: the remedy case (no entry-file path) and the new follow-the-printed-route control (`[]` pairs parsed, against the 2 expected). The other 4 stay green. - Every restore was proven: blob equal to HEAD and `git diff HEAD` empty. ## Gates Derived from this diff with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: the change set is 7 paths against the merge base `05be35259`, giving 88 commands. All 88 were run at `c2e5b3b81`. The `--ran` reconciliation reads **88 derived — 86 run, 2 NOT-MEASURED, 0 UNRUN**. - **86 exit 0.** These include `check:migration-registry`, `check:authorable-surface`, `check:api-surface`, `check:docs`, `check:doc-authoring`, `check:nul-bytes`, `check:empty-changeset`, `check:changeset-no-major`, `check:cross-package-test-inputs`, `check:test-source-alias`, `check:type-check-coverage` and `check:issue-citations`. Three of them needed a second run: - `check:doc-formula-expressions` and `check:lean-entry-closure` first refused with exit 3 (prerequisite not met). Both passed after a `turbo run build` of their 14-package closure. - `check:pm-dispatch-gates` was first cut by this run's own 900 s per-command cap, which is not counted. The uncapped re-run printed `dispatch-gates self-test: 1976 cases pass` in 1259 s. Its exit 0 is read from the absence of pnpm's failure marker, not from a captured status. - **NOT MEASURED:** `check:dual-build-cjs-loads` and `check:type-check-debt`. Both exit 3 (prerequisite not met): they read every workspace package's `dist/`, which `lint.yml` builds first, and that whole-tree build was not run in this worktree on a shared 4-core box. CI runs them. - `pnpm --filter @objectstack/spec check:generated` (named by the dispatch): "All 15 generated artifacts are up to date", exit 0. - `origin/main` advanced 11 commits past the base during these runs. None of them touches these 7 paths or `src/migrations/`, so they are not merged here; the PR's CI runs on the merge ref. ## Acceptance notes - `scripts/lib/sharded-artifacts.ts` (about `:353`) writes the manifest shard `description` into every `json-schema.manifest/` shard, and that text still says "must be declared in RETIRED_DEFS_BY_MAJOR (src/migrations/registry.ts)". It is the same family as the H1 rows. It is not changed here, because changing it regenerates every manifest shard, the hottest generated artifact. Carrier: none. - `src/data/object.zod.ts`'s `UNKNOWN_KEY_GUIDANCE` has a section comment, "Tombstones for RETIRED keys", that uses the same loose term the two docblocks used. The card scoped this to two docblocks with no table edits. Carrier: none. - `scripts/lib/default-changes.ts` (about `:52`) has a code comment that still says "semantic entry to the major's step in src/migrations/registry.ts". The printed remedy it describes is fixed here. Carrier: none. - The changeset follows the dispatch's H3 rule (shipped text ⇒ `patch`) and AGENTS.md's `skip-changeset` criterion ("a diff that publishes nothing from any released package"). os-dev's fast path lists comments as non-publishing; this PR measured the shipped bytes instead. ## Review A docblock edit in `packages/spec/src` owes an at-tier contract review at landing (run by the seat). No governed surface is touched. --- _Generated by [Claude Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20803
Clause-②: no
Dispatched by the
domain:skillsseat 1 (seat post #7623) under the PM claim 5907960440, sessionsession_01KTZmMfzVzjNvyaLyQ8mHvg, branchclaude/issue-20803-retirement-kit-registry-entries. This is a draft. The file is on a governed surface (.claude/**, Tier S in theGOVERNED_SURFACESregister), so the seat lands it after an at-tier contract review.维护者速读(草稿)
.claude/skills/spec-property-retirement/SKILL.md的两段文字。§3 把「退役键登记」改成在entries/retired-keys/加一个文件、再跑gen:migration-registry;§2 的枚举值退役改指现成的enumWithRetiredValues(),不再教手写errormap。文件行数不变(337/337)。registry.ts早已改为由entries/逐文件生成,旧文字却让照做的 agent 往生成区手打,check:migration-registry必红、白费一轮;枚举值退役已有通用 helper,旧文字仍在教旧的手写形状。What changed
One file,
.claude/skills/spec-property-retirement/SKILL.md, two texts, net 0 lines (337 before and after; ceiling 337). The landing site is the one the claim named. The measurement found no other producer of the wrong text inside.claude/**.Text 1: §3, the
RETIRED_KEYS_BY_MAJORchecklist item. It sent the entry to be typed intopackages/spec/src/migrations/registry.ts, and it said the gate's failure output prints "the line to paste". That table is now generated: it is concatenated from one file per entry, and the generator reverts a hand edit between the markers, whichcheck:migration-registryreports first. The item now says what an author actually does:packages/spec/src/migrations/entries/retired-keys/, named asentries/README.mdsays. The kit does not restate the naming convention.pnpm --filter @objectstack/spec gen:migration-registry, which generates the entry intoregistry.ts. Never type the entry there.Kept unchanged:
'DEFKEY:NAME'value, spelled asauthorable-surface/CATEGORY.jsonspells it (minus[RETIRED]), filed under the current major;Two lines of the old item are byte-identical in the new one: the
authorable-surface/…line and the (b2) tail line.Deleted: "the gate's failure output prints the line to paste". That sentence named the paste route this card retires, and what gate (b) prints is #20809's to change. The new text is true both before and after #20809 lands, because it names the file route and never quotes the gate's current wording as the thing to follow.
Text 3: §2, the enum-value paragraph. It prescribed a hand-rolled enum
errormap dispatched onissue.input, citingHookBodyCapability(data/hook-body.zod.ts) and theobject.managedBy: 'system'precedent. It now points atenumWithRetiredValues()inshared/retired-key.ts: declare the retired members and their prescriptions there, with no hand-writtenerrormap.Kept: an enum-value narrowing is invisible to the four ratchets, and none of the three key routes applies to it.
Deleted: the two hand-rolled citations. Offered as the shape to copy, they now point at the wrong route. They also have another home: the helper's own docblock (
retired-key.ts, "The gap this closes") names both as the hand-rolled precursors of the helper.Measured before writing (
origin/mainat79114850)The registry is generated.
packages/spec/src/migrations/registry.ts:19-26says the three tables are GENERATED from./entries/, one file per entry, "never by typing between the markers. See./entries/README.md." There are 237 files underentries/retired-keys/.The README owns the route.
entries/README.md"Adding an entry" gives the file-name rule and thegen:migration-registrycommand. Its "Four rules" say that the nextgen:run reverts a hand edit inside the markers, and thatcheck:migration-registry(tsx scripts/build-migration-registry.ts --self-test --check) reports it before that.The helper has no production caller.
HookBodyCapability(data/hook-body.zod.ts:48-60) andobject.managedBy(data/object.zod.ts:1705-1710) are still hand-rolledz.enum(values, { error })maps.enumWithRetiredValues()has 0 non-test call sites.git grepoutside*.test.tsfinds only its definition and docblock inretired-key.ts, and one test file uses it (shared/retired-key.test.ts).The helper covers every retirement the hand-rolled map covers. Both hand-rolled sites answer one exact retired string with a prescription and leave every other value to zod, which is exactly the helper's
retiredmap. The helper's docblock names one case it does not cover: a retired member that WAS the default. A hand-rolled map cannot cover that case either, because the default must be a live member. The docblock routes that case toacceptRetiredDefaultResidue. So the kit says "don't hand-roll" with no exception, and stays true.No other sentence sends an entry into
registry.ts. The census covered.claude/**,AGENTS.mdandCLAUDE.md, grepping formigrations/registry,registry.ts,RETIRED_KEYS_BY_MAJOR,RETIRED_DEFS_BY_MAJOR,copy these linesand粘贴, plus paste/append near registry. It found tworegistry.tshits, both in the kit:MIGRATIONS_BY_MAJORstep'srationaleandconversionIdsare hand-written outside the generated markers (registry.ts:37-42;STEP18_RATIONALEat:5024).The kit's D3
semanticitem already routes throughentries/, and nothing outside the kit matched.One route, said once in each place (with #20809)
At this head, #20809 has no branch or PR, so this lands first. Under the triage split the second to land quotes the first, so gate (b)'s printed remedy should name the same directory and command:
packages/spec/src/migrations/entries/retired-keys/, with the file name perentries/README.md;pnpm --filter @objectstack/spec gen:migration-registry.#20809 remains open. This PR touches nothing under
packages/spec/**.Line budget
Changeset
None: the diff publishes nothing.
.claude/**ships in no package'sfiles[]:package.jsonmanifests, 0 of whosefiles[]names.claude;private;.claude/directory sits inside any package directory (git ls-filesshows 50 tracked files under the root.claude/only);@objectstack/spec'sfiles[]readsdist,json-schema,liveness,prompts,llms.txt,README.md,src/**/*.zod.ts,CHANGELOG.md,api-surface,spec-changes.json.The seat applies
skip-changeset; this PR writes no label.Gates (at head
21db5689)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 18 commands, the same set as the dispatch. I ran all 18 plus three extras,check:pm-settings-deny-roster,check:pm-skill-id-lintandcheck:pm-governed-prose. Every one exited 0.dispatch-gates --ranwith the recorded exit codes reported "18 derived famil(ies) accounted for — 18 run, 0 NOT-MEASURED (a DERIVED zero)".check:pm-skill-ratchetexit 0: "spec-property-retirement/SKILL.md is 337 lines (ceiling 337; headroom 0)"; "widest table row is 324 bytes (pin 326; headroom 2)".check:skill-frame-syncexit 0;check:doc-authoringexit 0;check:nul-bytesexit 0 ("no raw ASCII control bytes");check:pm-skill-id-lintexit 0 ("34 file(s) clean").check:pm-governed-mergesexit 0 ·check:pm-governed-proseexit 0 ·check:pm-settings-deny-rosterexit 0.pnpm --filter @objectstack/lint run check:doc-formula-expressionsexit 0, after aturbo run build --filter='@objectstack/lint...'(4 tasks, exit 0) underos-verify-lock.sh.check-ci-filter-parity·check-closing-keyword-parity(+--self-test) ·check-comment-mask-corpus·check-harness-current --self-test·check:agent-test-spelling·check:cross-package-test-inputs·check:driver-memory-census·check:gitlink-declared·check:refd-timer-probe·check:required-contexts·check:watch-hint-literal: each exit 0.Acceptance notes
packages/spec/scripts/build-schemas.ts:2636-2640is the def-removal gate. It prints the same "copy these lines in … under18: [ … ]" remedy forRETIRED_DEFS_BY_MAJOR, whose table is generated fromentries/retired-defs/. spec: gate (b) of build-schemas.ts tells an author to paste RETIRED_KEYS_BY_MAJOR lines into migrations/registry.ts, which is generated from entries/ since #7297; two guidance-slot docblocks describe the old retirement route (spec half of #20803) #20809 lists only gate (b) (:1127-1128), so its author may want to take this sibling in the same edit. Not filed separately.enumWithRetiredValues()still has no production call site, and its docblock@exampleshowsHookBodyCapabilitybuilt with it, whichmaindoes not do. Both hand-rolled precursors work as they are; this is an observation, not a defect. Carrier: none.Generated by Claude Code