Skip to content

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

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20803-retirement-kit-registry-entries
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20803-retirement-kit-registry-entries

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20803

Clause-②: no

Dispatched by the domain:skills seat 1 (seat post #7623) under the PM claim 5907960440, session session_01KTZmMfzVzjNvyaLyQ8mHvg, branch claude/issue-20803-retirement-kit-registry-entries. This is a draft. The file is on a governed surface (.claude/**, Tier S in the GOVERNED_SURFACES register), so the seat lands it after an at-tier contract review.

维护者速读(草稿)

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_MAJOR checklist item. It sent the entry to be typed into packages/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, which check:migration-registry reports first. The item now says what an author actually does:

  • add one file under packages/spec/src/migrations/entries/retired-keys/, named as entries/README.md says. The kit does not restate the naming convention.
  • run pnpm --filter @objectstack/spec gen:migration-registry, which generates the entry into registry.ts. Never type the entry there.

Kept unchanged:

  • the literal 'DEFKEY:NAME' value, spelled as authorable-surface/CATEGORY.json spells it (minus [RETIRED]), filed under the current major;
  • gate (b) reads it by exact set membership, with no inference;
  • the (b2) warning: do not add the entry before the tombstone lands;
  • the "why a second table" note and the aging-clock note.

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 error map dispatched on issue.input, citing HookBodyCapability (data/hook-body.zod.ts) and the object.managedBy: 'system' precedent. It now points at enumWithRetiredValues() in shared/retired-key.ts: declare the retired members and their prescriptions there, with no hand-written error map.

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/main at 79114850)

  • The registry is generated. packages/spec/src/migrations/registry.ts:19-26 says 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 under entries/retired-keys/.

  • The README owns the route. entries/README.md "Adding an entry" gives the file-name rule and the gen:migration-registry command. Its "Four rules" say that the next gen: run reverts a hand edit inside the markers, and that check: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) and object.managedBy (data/object.zod.ts:1705-1710) are still hand-rolled z.enum(values, { error }) maps. enumWithRetiredValues() has 0 non-test call sites. git grep outside *.test.ts finds only its definition and docblock in retired-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 retired map. 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 to acceptRetiredDefaultResidue. 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.md and CLAUDE.md, grepping for migrations/registry, registry.ts, RETIRED_KEYS_BY_MAJOR, RETIRED_DEFS_BY_MAJOR, copy these lines and 粘贴, plus paste/append near registry. It found two registry.ts hits, both in the kit:

    • the text-1 line, fixed here;
    • the step-18 D3-chain item. That item is true and stays untouched: a MIGRATIONS_BY_MAJOR step's rationale and conversionIds are hand-written outside the generated markers (registry.ts:37-42; STEP18_RATIONALE at :5024).

    The kit's D3 semantic item already routes through entries/, 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:

  • directory: packages/spec/src/migrations/entries/retired-keys/, with the file name per entries/README.md;
  • command: pnpm --filter @objectstack/spec gen:migration-registry.

#20809 remains open. This PR touches nothing under packages/spec/**.

Line budget

  • Lines: 337 / 337 (ceiling 337, headroom 0). Text 1 went from 6 lines to 7, and text 3 from 4 lines to 3.
  • Line width: the widest new line is 116 bytes, and every new non-table line is 120 bytes or less.
  • Table rows: none touched. The widest is 324 bytes against the 326 pin.
  • No re-wrap of untouched lines. Every deleted clause is false or has another home (see above).

Changeset

None: the diff publishes nothing. .claude/** ships in no package's files[]:

  • 69 non-private package.json manifests, 0 of whose files[] names .claude;
  • the root package is private;
  • no .claude/ directory sits inside any package directory (git ls-files shows 50 tracked files under the root .claude/ only);
  • positive control: @objectstack/spec's files[] reads dist, 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-lint and check:pm-governed-prose. Every one exited 0. dispatch-gates --ran with the recorded exit codes reported "18 derived famil(ies) accounted for — 18 run, 0 NOT-MEASURED (a DERIVED zero)".

  • check:pm-skill-ratchet exit 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-sync exit 0; check:doc-authoring exit 0; check:nul-bytes exit 0 ("no raw ASCII control bytes"); check:pm-skill-id-lint exit 0 ("34 file(s) clean").
  • check:pm-governed-merges exit 0 · check:pm-governed-prose exit 0 · check:pm-settings-deny-roster exit 0.
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions exit 0, after a turbo run build --filter='@objectstack/lint...' (4 tasks, exit 0) under os-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


Generated by Claude Code

…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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 21db56899a0085212b9627a60e4238510a973380
Local-runs: none

Read against: PR #20828 head 21db56899a0085212b9627a60e4238510a973380 (branch claude/issue-20803-retirement-kit-registry-entries, head repo = base repo, draft, 2 commits, 1 file, +8 / -8); origin/main at 72f8c3820154c69cdf9faa6f887dc544e4c4b823; merge-base 79114850f0f559dcb4fb432f5bcfc12947a03de6 (the dev's stated base). The kit blob at origin/main is 738187f1, the diff's own old side, so the PR's net diff against main is the one file, 8 / 8, exactly as GitHub lists it. Inputs: card #20803 body and its four comments (5906870733, 5907757995, 5907960440, 5908302870); #20809 body and pointer 5908348030 for context; PR body, file list, diff; PR thread: 0 comments. The template print (record-recognisers.mjs --template) is the brief's own prescribed read for this record's shape and evaluates nothing on the diff; every measurement below is wc / awk over git show output at the head or at main.

Check-runs on the head (filter=latest): 35 runs. 24 success, 11 skipped, 0 in any other conclusion. No job is outside success / skipped. Required contexts: Lint & Repo Gates success; TypeScript Type Check success; Test Core success (all six shards success); Dogfood Regression Gate success; Governed Surface Queue Guard success; Build Core skipped; Temporal Conformance (live PG + MySQL) skipped. The two skips are paths-filter skips on a .claude/**-only diff, not failures. Nothing is red.

① Derived judgments

Text 1: §3, the RETIRED_KEYS_BY_MAJOR item (head L182-188). Each statement checked at the code on origin/main 72f8c382; the registry header is byte-identical at the merge-base, and every line number the dev cited resolves at both.

Text 3: §2, the enum-value paragraph (head L141-143).

  • "hang the prescription on the enum itself: enumWithRetiredValues() (shared/retired-key.ts) declares the retired members and their prescriptions": RIGHT. retired-key.ts:321-365: enumWithRetiredValues(values, retired) returns z.enum(values, { error }) whose map answers a retired member with its prescription and lets every other input fall through to zod's own message (:359-363).
  • "no hand-written error map" (「⛔ 不再手写 error map」): RIGHT; the helper's contract covers both hand-rolled sites. HookBodyCapability (data/hook-body.zod.ts:48-60) answers exactly one string, 'crypto.hash', with CRYPTO_HASH_RETIRED; object.managedBy (data/object.zod.ts:1705-1711) answers exactly 'system' with MANAGED_BY_SYSTEM_RETIRED; both return undefined otherwise. The helper is that map generalised to N members (string-guarded hasOwnProperty lookup) plus three construction-time refusals a hand-rolled map lacks (an empty map, a "retired" member still in values, a blank prescription). It returns a plain ZodEnum, so .optional().describe(...) as managedBy uses it composes (docblock :285-288). Nothing either hand-rolled map does is outside the helper's reach.
  • The disclaimed case (a retired member that WAS the default): the dev's claim that a hand-rolled map cannot reach it either is RIGHT. An enum error map runs only when a present value fails the enum: an omitted key materialises .default() without touching the map, and a released artifact carrying the retired default materialised is refused by ANY enum-level map, hand-rolled or helper. That population needs accept-and-strip ahead of the shape, which is acceptRetiredDefaultResidue (:189-218, a z.preprocess wrapper on the object, live at connector.zod.ts and permission.zod.ts), a different layer, not an error map. So "don't hand-roll" holds with no exception; the routing of the default case lives in the helper's docblock (:288-294) and was never in the kit, before or after.
  • Kept: an enum-value narrowing is invisible to the four ratchets: still RIGHT at the code. authorable-surface/ records ${defKey}:${name} per property (build-schemas.ts:957-963); json-schema.manifest/ shards list def keys ("schemas": ["data/Address", …]); api-surface/ rows are name (kind); api-surface-signatures.json hashes checker.typeToString() of each defineX factory, which "prints a type REFERENCE and does not expand it … Value-level narrowing (an enum losing a member) is still ungated" (build-api-surface.ts:32-40, :177). The helper's docblock :300-303 restates the same.
  • Kept: none of the three key routes applies to "def survives, one member leaves": RIGHT, helper docblock :257-259.
  • Deleted: 「按 issue.input 分派」. Not false; that dispatch is now the helper's internals (:359-363). Its home is the helper. Not a lost rule for an author.
  • Deleted: the two citations 「packages/spec/src/data/hook-body.zod.ts 的 HookBodyCapability,沿用 object.managedBy: 'system' 的先例」. As the shape to copy they now point at the wrong route; as precedents they still exist on main (both still hand-rolled) and are named as the hand-rolled precursors in the helper's "The gap this closes" (:255-268). Home kept; not lost.

Pins. Kit line count 337 at origin/main and 337 at the head (wc -l over git show); the ratchet rows are unchanged (scripts/pm/check-skill-line-ratchet.mjs:1377 ceiling 337, :2022 table-row pin 326) and the PR's file list is the kit alone. Added lines measured at 99 / 113 / 73 / 104 / 116 / 112 / 97 / 81 bytes: widest 116, all at or under MAX_LINE_BYTES = 120 (:2251). The only non-table line over 120 bytes at the head is L114 (311 bytes), a blockquote line (leading greater-than sign), present at main at the same line and structurally exempt (classifyLine 'blockquote', :2476). No table row touched; widest table row 324 bytes at L86 against pin 326. Text 1 went 6 to 7 lines, text 3 went 4 to 3: net 0, as the report says.

② Semver level

None: the diff publishes nothing. One file, .claude/skills/spec-property-retirement/SKILL.md, the only file in that directory; git grep for .claude over every package.json on main is empty and the root package is private. skip-changeset matches; Check Changeset is success on the head. Clause-②: no is right: no authorable key, export or config field changes.

③ Boundary flags

  1. Tier naming. Triage unlock 5907757995 calls the card "a governed surface, Tier H". The register rules: scripts/pm/check-governed-merges.mjs:1094 row claude-tree, glob .claude/**, GOVERNED_TIER_S; AGENTS.md PD feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14 puts all of .claude/** in Tier S. The PR is Tier S; the dev's deviation note is right, and leaving another seat's comment alone was right. This record is the correction of record for the PR.
  2. RETIRED_DEFS_BY_MAJOR paste remedy. Verified at build-schemas.ts:2636-2639: the def-removal gate prints the same "copy these lines in … under N: [ … ]" for a table generated from entries/retired-defs/; :789 names RETIRED_DEFS_BY_MAJOR (src/migrations/registry.ts) without saying to type there. packages/spec/**, domain:spec, outside this PR's claimed surface; carried to 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 as pointer 5908348030, which the seat posted. Escalated; nothing to do here.
  3. enumWithRetiredValues() has 0 production call sites. Verified by git grep at origin/main 72f8c382: the definition and its docblock @example in retired-key.ts, plus one test file; HookBodyCapability and managedBy remain hand-rolled. This does not make the kit false: the kit prescribes the route for the NEXT narrowing, and the helper's docblock names both sites as the precursors it replaces. Converting them is a no-behaviour-change refactor, not a defect under PD chore: version packages #10; "noted, not filed" is the right disposition. Carrier: none.
  4. Table-row pin headroom. Widest row 324 at L86 against pin 326 (:2022); the ratchet prints a lower-the-pin hint (:2592) and stays green. The row is untouched context in this diff and the headroom predates it; the claim limited ratchet edits to a lowered LINE ceiling, so leaving it is per the claim. Answered; a later edit to the file may lower it opportunistically.
  5. Two required contexts read skipped. Build Core and Temporal Conformance (live PG + MySQL) are skipped on this head (paths filter, .claude/**-only diff). Not red; recorded so the landing seat reads the state as it is.
  6. Commit trailers and extra gates. Both commits carry the model-free pair (Claude-Session: plus Co-authored-by: Claude), per AGENTS.md; the dev's deviation note about the harness reminder is right. The three extra gates run beyond the derivation are harmless.
  7. Coordination with 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. 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 still has no branch or PR; this PR lands first and 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 quotes it (pointer 5908348030 already does). Nothing under packages/spec/** is touched here; the file list is one path.

Landing. Tier S: with this PASS on head 21db5689 and every check-run at success / skipped, the landing key holds; the owning seat lands it through the queue (ready first, arm second), and the post-merge audit is the compensating control.

Implemented-by: claude/issue-20803-retirement-kit-registry-entries
Reviewed-by: session_01KTZmMfzVzjNvyaLyQ8mHvg

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 14:06
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 745ac30 Sep 30, 2026
39 of 40 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20803-retirement-kit-registry-entries branch September 30, 2026 14:31
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants