Skip to content

Commit 47a9002

Browse files
os-steveclaude
andauthored
docs(spec): reconcile the RETIRED_KEYS_BY_MAJOR Lifecycle docblock with check (b3) (#18967)
Part of #18624 — item ① only; the card stays open for the half named below. Clause-②: no `RETIRED_KEYS_BY_MAJOR`'s docblock is shipped text: it reaches consumers in `@objectstack/spec`'s `dist/index.d.ts`, measured by grepping the built `.d.ts` for a phrase from this very paragraph against a positive control. Since check (b3) landed, two of its sentences were false, and one of them contradicted a remedy the gate prints. ## What was false | docblock said | check (b3) does | |:---|:---| | 「The one state the gate rejects is an entry naming a key that is still LIVE」 | rejects a **second** state — a NESTED row whose def this build emits but whose dotted path it does not | | 「Entries are permanent」 | its refusal text ends `… or delete the entry from packages/spec/src/migrations/registry.ts` | | 「its entry here stays, and then names a key the build no longer emits — the expected steady state, not an error」 | true only for a TOP-LEVEL key; a nested row never reaches `authorable-surface/` at all, so it has no aging clock and can never be in that steady state | An author following the docblock would not delete the row; an author following the gate would. Both texts ship in the same repository, so two people each doing as they were told go opposite ways. ## What changed One file, `packages/spec/src/migrations/registry.ts`, comment text only — the hand-written docblock above the declaration, outside the `os-generated` regions (`check:migration-registry` green confirms nothing generated moved). - The `## Lifecycle` paragraph scopes the aging-out steady state to a **top-level** tombstone and says why a nested row can never be in it. - It lists **both** rejected states with the check that owns each and the remedy that check actually prints: still-LIVE is check (b2), nested-and-unresolvable is check (b3). - It states the routing rule that decides which check judges a row — a row is read as a path only when its `name` half carries a dot **and** this build emits no top-level property of that exact name, so a live dotted top-level key such as the OData context key stays on the map check (b2) has always judged it on. That mirrors `build-schemas.ts`'s `!currentKeys.has(k) && isNestedAuthorableKey(k)` routing rather than paraphrasing it as "has a dot". - It reconciles permanence with deletion instead of leaving the two to contradict: a row that was ever TRUE of some build is history and is never deleted; a row check (b2) or (b3) refuses was never true of any build, so deleting it removes a false claim rather than a record. - It repeats check (b3)'s own ⛔ — that check cannot yet tell a wrong row apart from every truthful one, and for the shapes its refusal enumerates the remedy is to teach the check, never to delete a row that is telling the truth. - The `## What reads it` bullet for check (b) and the `@see` roster name check (b3), which reads this table and was named by neither. ⛔ No gate behaviour moves. No schema, export, registry entry or refusal condition is touched; the set of metadata that validates is byte-for-byte what it was. The changeset is a `patch` because the shipped `.d.ts` text moves, not because anything executes differently. ## Sweep — the other carriers of the same sentences Checked before editing, since fixing one carrier and leaving three is the failure mode this card is an instance of: - `RETIRED_DEFS_BY_MAJOR`'s parallel `## Lifecycle` paragraph in the same file carries nearly identical wording. It is **still true** and is deliberately untouched: check (b3) judges nested KEYS, and that table's only consumer remains the manifest deletion gate. - `packages/spec/scripts/lib/nested-authorable-keys.ts` already states the three-state distinction correctly and at length; nothing there is stale. - `packages/spec/src/migrations/entries/README.md` describes the entry-file layout only and makes no lifecycle claim. - No test, generated doc or `content/docs/` page quotes the paragraph: the phrases 「Entries are permanent」 and 「The one state the gate rejects」 occur in this file and nowhere else in the tree. ## What is deliberately NOT in this PR The card carries three items; this delivers item ① and no more, which is why the first line reads Part of rather than binding a closing keyword to the card. - **Item ②** — check (b3)'s refusal enumerates two legitimate shapes it cannot tell apart from a wrong row, and a third exists (an ancestor of a registered nested key tombstoned in place, removed via the guidance route, or reached through a `$ref` to a tombstoned `$def`). Re-measured here and structurally real: a tombstoned ancestor node renders as `{ "not": {} }`, which carries no `properties`, so any registered descendant path under it lands in the same `unresolvable` state. It is unreached on this tree. Its carrier is the refusal text in `packages/spec/scripts/build-schemas.ts`, **outside this PR's declared file surface** (`packages/spec/src/migrations/**` plus any sibling test pinning the wording), and writing a new enumerated shape is authored prose rather than a mechanical repair — so it is reported rather than taken here. - **Item ③** — re-measured first-hand and confirmed: `packages/spec/scripts/lib/nested-authorable-keys.ts` contains `items` 4 times and `prefixItems` 0 times, so a tuple-shaped nested row would not be seen. The card records this as a limit of sight rather than a defect and asks for no action. ## Verification All readings taken in a dedicated worktree at head `f75798a2`, after merging `origin/main`. - `pnpm --filter @objectstack/spec build` — exit 0; 34/34 declaration files present. - `pnpm --filter @objectstack/spec check:generated` — exit 0; **all 15** generated artifacts up to date, `check:migration-registry` among them. - `pnpm --filter @objectstack/spec typecheck` — exit 0 (includes `check:scripts-typecheck` and `check:test-typecheck`). - `pnpm --filter @objectstack/spec test` — exit 0; **488 test files, 14182 tests passed**. - `pnpm lint` (the repo-wide union, `eslint . --no-inline-config`) — exit 0, re-run on this exact head after the merge, 71s. - Gate families derived mechanically from the change set with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack`, re-derived after the changeset existed and again after the merge, then reconciled with `--ran`: **78 derived, 75 run green, 3 NOT MEASURED, 0 UNRUN.** Every exit code was captured before any pipe. The three NOT MEASURED are all `exit 3` = PREREQUISITE NOT MET, which is neither a pass nor a failure: - `pnpm check:dual-build-cjs-loads` — needs a whole-repo `pnpm build` (87 packages have no `dist/`). - `pnpm check:lean-entry-closure` — needs `@objectstack/objectql` built. - `node scripts/check-plugin-teardown-shape.mjs --self-test` — its positive-control fixture is pinned to a commit this shallow checkout cannot reach; the gate's non-self-test run was green. This one grades the checker's fixtures, not this diff. CI covers all three. No reverse-verification or ablation run is reported because none is owed: this diff adds no gate, changes no cross-package type, and has no behaviour whose removal could turn a test red. ## Acceptance notes - `.claude/skills/spec-property-retirement/SKILL.md` §3 warns a retirement author only about check (b2) ("⚠ **不要**在墓碑落地前先加条目") and never mentions check (b3). That is **incomplete, not false** — no sentence there is contradicted — so by the filing rule it is noted rather than filed. Carrier: a governed surface, outside this card's file surface. Whoever picks up item ② is the natural taker, since that is the next edit to touch (b3)'s author-facing text. --- _Generated by [Claude Code](https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 26c73fb commit 47a9002

2 files changed

Lines changed: 59 additions & 9 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): the `RETIRED_KEYS_BY_MAJOR` Lifecycle docblock names both rejected states, and stops contradicting check (b3)'s printed remedy
6+
7+
`RETIRED_KEYS_BY_MAJOR`'s docblock is shipped text — it reaches consumers in `dist/index.d.ts` — and since check (b3) landed, two of its sentences were false:
8+
9+
- **「The one state the gate rejects」**. Check (b3) rejects a *second* state: a NESTED row whose def this build emits but whose dotted path it does not. That state has no aging clock behind it (a nested key never reaches `authorable-surface/` at all), so it is not the aged-out steady state the paragraph described.
10+
- **「Entries are permanent」**, against check (b3)'s own refusal text, which ends `… or delete the entry from packages/spec/src/migrations/registry.ts`. An author following the docblock would not delete; an author following the gate would — two shipped instructions in this repo pushing two people who each did as they were told in opposite directions.
11+
12+
The Lifecycle paragraph now:
13+
14+
- scopes the aging-out steady state to a **top-level** tombstone, and says why a nested row can never be in it;
15+
- lists **both** rejected states with the check that owns each and the remedy that check prints — still-LIVE (b2), nested-and-unresolvable (b3) — and states the routing rule that decides which one a row is judged by (a row is read as a path only when its `name` half carries a dot AND this build emits no top-level property of that exact name, so a live dotted top-level key such as `@odata.context` stays on (b2)'s map);
16+
- reconciles permanence with deletion instead of leaving them to contradict: a row that was ever TRUE of some build is history and is never deleted, while a row (b2) or (b3) refuses was never true of any build, so deleting it removes a false claim rather than a record;
17+
- repeats (b3)'s own ⛔ — it cannot yet tell a wrong row apart from every truthful one, and for the shapes it names the remedy is to teach the check, never to delete a row that is telling the truth.
18+
19+
The `## What reads it` bullet for check (b) and the `@see` roster gain (b3) for the same reason: it reads this table, and neither named it.
20+
21+
**No behaviour moves.** No gate, schema, export or registry entry is touched — the set of metadata that validates is byte-for-byte what it was. What changes is the text an author reads when a gate refuses their row.

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12140,8 +12140,12 @@ export const MIGRATION_MAJORS: readonly number[] = Object.keys(MIGRATIONS_BY_MAJ
1214012140
* gate stays red:
1214112141
*
1214212142
* - **Check (b)**, the retirement gate: a key that flips live → retired must
12143-
* appear here, by exact set membership, or the build fails. (Check (b2) is its
12144-
* inverse: an entry naming a key this build still emits as LIVE is rejected.)
12143+
* appear here, by exact set membership, or the build fails. Two companions
12144+
* read the table in the other direction: check (b2) refuses an entry naming a
12145+
* key this build still emits as LIVE, and check (b3) — nested rows only —
12146+
* refuses a dotted entry whose def this build emits but whose path it does
12147+
* not (#17969). See "Lifecycle" below for what each of them means for a row
12148+
* already in the table.
1214512149
* - **Check (c)**, the baseline-deletion gate (#4650): the major recorded here
1214612150
* is what starts a tombstone's ~two-major aging clock, so it is also what
1214712151
* eventually lets its `authorable-surface/` line be deleted. Since #5898 —
@@ -12206,14 +12210,39 @@ export const MIGRATION_MAJORS: readonly number[] = Object.keys(MIGRATIONS_BY_MAJ
1220612210
*
1220712211
* ## Lifecycle
1220812212
*
12209-
* Entries are permanent. A declared tombstone ages out after ~two majors and its
12210-
* line may then leave `authorable-surface.json` (check (c)); its entry here
12211-
* stays, and then names a key the build no longer emits — the expected steady
12212-
* state, not an error. The one state the gate rejects is an entry naming a key
12213-
* that is still LIVE: a registration nothing consumed, pre-approving a
12214-
* retirement that has not happened.
12213+
* A TRUE entry is permanent. A declared TOP-LEVEL tombstone ages out after ~two
12214+
* majors and its line may then leave `authorable-surface.json` (check (c)); its
12215+
* entry here stays, and then names a key the build no longer emits — the
12216+
* expected steady state, not an error. A NESTED row never reaches that file at
12217+
* all (0 dotted entries on the shipped surface), so it has no aging clock and
12218+
* "the build no longer emits it" is never that steady state for one.
1221512219
*
12216-
* @see scripts/build-schemas.ts — checks (b)/(b2)/(c), the only consumers
12220+
* Two states are rejected. A row is read as a PATH only when its `name` half
12221+
* carries a dot AND this build emits no top-level property of that exact name,
12222+
* so a live dotted TOP-LEVEL key (`@odata.context`, a SCIM extension URN) stays
12223+
* on the map check (b2) has always judged it on:
12224+
*
12225+
* - **Still LIVE** (check (b2)): the entry names a key this build still emits as
12226+
* writable — a registration nothing consumed, pre-approving a retirement that
12227+
* has not happened. Remedy: tombstone the key, or delete the entry.
12228+
* - **Nested and unresolvable** (check (b3), #17969): the entry's def IS emitted
12229+
* and the dotted path is not in it. Because checks (a0)/(a)/(b)/(c) are
12230+
* structurally blind to a nested key, no gated route can produce this state —
12231+
* the row is wrong (a typo'd def, a typo'd path, a stale key name). Remedy:
12232+
* fix the spelling against the emitted schema, or delete the entry. A row
12233+
* whose DEF this build does not emit is NOT this state: that is the whole-def
12234+
* removal steady state, registered in {@link RETIRED_DEFS_BY_MAJOR}.
12235+
*
12236+
* ⛔ "Delete the entry" and "entries are permanent" do not conflict, and reading
12237+
* one as licence to drop the other is the way to lose a retirement. A row that
12238+
* was ever TRUE of some build is history and is never deleted; a row check (b2)
12239+
* or (b3) refuses was never true of any build, so deleting it removes a false
12240+
* claim rather than a record. Check (b3) can also not yet tell a wrong row apart
12241+
* from every truthful one — its refusal text enumerates the shapes it knows it
12242+
* cannot see — and for those the remedy is to teach the check the shape, ⛔ never
12243+
* to delete a row that is telling the truth.
12244+
*
12245+
* @see scripts/build-schemas.ts — checks (b)/(b2)/(b3)/(c), the only consumers
1221712246
*/
1221812247
export const RETIRED_KEYS_BY_MAJOR: Readonly<Record<number, readonly string[]>> = {
1221912248
// The first entries since #4659 built this table (#5552). ONE tombstone

0 commit comments

Comments
 (0)