Skip to content

feat(spec): a retired key's tsc error names the retirement and points at os validate (#20621) - #21023

Merged
objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-20621-retired-key-type-names-retirement
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-20621-retired-key-type-names-retirement

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20621
Clause-②: yes

What changed

retiredKey() (packages/spec/src/shared/retired-key.ts) now declares an explicit return type. The input and the output of every tombstoned key are both an object type with one property, typed never, whose name is the retirement sentence. Writing the key now fails tsc with an error that names the retirement, for every kind of value:

TS2741: Property ''[REMOVED] Key retired: run `os validate` for its migration.'' is missing in type 'string[]' but required in type '{ '[REMOVED] Key retired: run `os validate` for its migration.': never; }'.
TS2322: Type 'string' is not assignable to type '{ '[REMOVED] Key retired: run `os validate` for its migration.': never; }'.
TS2353: Object literal may only specify known properties, and 'strategy' does not exist in type '{ '[REMOVED] Key retired: run `os validate` for its migration.': never; }'.

Before this change, every one of these errors read Type 'string[]' is not assignable to type 'undefined'.

Runtime is unchanged. The schema is still the same z.never().optional().describe('[REMOVED] …'). The return type is an upcast that needs no cast, because never is assignable to the mark. The parse error, its prescription, the text os validate prints, the D2 conversions, acceptsNothing and the JSON-schema/authorable-surface walkers all see what they saw before. No value can have the mark's type without a cast, so tsc still accepts only absence.

Measured channel table

Probes: a scratch tsc project per design over zod directly, plus the real built @objectstack/spec dist with a defineStack stack. The stack carries a page's assignedProfiles (PageSchema, a strictObject, array value), a field's conditionalRequired (FieldSchema, a strictObject, string value, a rename retirement) and an index's type (IndexSchema, a non-strict z.object, string value). Strict and non-strict printed identically in every design.

design literal-guidance site concatenated / constant / helper site (all 287 in the tree) strictObject vs z.object tsc channel hover channel
base: input undefined not assignable to type 'undefined' same same ✗ names nothing key?: undefined ✗
generic brand over the guidance literal (named interface keyed by a unique symbol) TS2741 names RetiredKeyG with the text inside its type argument, truncated by tsc at about 330 chars RetiredKeyG of string: no text, and the string argument reads as "wants a string" same ✓ only at literal sites, which are 0 of 287 name only
fixed-name brand interface RetiredKey names RetiredKey (the sentence prints only for array values, if it is the property name) same same ~ the name only key?: RetiredKey ~
template literal type, [REMOVED] followed by G full text, untruncated the template over string; it admits any string with that prefix, which the parse refuses, and steers a string author toward writing one same ~, but declares a value the runtime refuses same as tsc
alias of undefined, or undefined intersected with a brand collapses to undefined same same ✗ ✗
@deprecated JSDoc reaches hover only if written by hand at each of the 287 sites; tsc never prints it (a suggestion diagnostic only; 6385 did not fire on an object-literal property in the probe) same same ✗ (hover-only, refused by triage) ~ per site
inline anonymous mark, input only sentence for every value kind same (text-independent) same ✓ ✓
shipped: inline anonymous mark, input and output sentence for every value kind same same ✓ ✓ on a z.input-annotated literal and inside defineStack objects

Call-site census: 287 retiredKey() calls in 72 files of packages/spec/src at 9525651cf7. 154 pass a '…' + '…' concatenation, 83 a constant, 49 a helper's return and 1 a concatenation with a non-literal operand. 0 pass a single literal, and the checker types the argument string at all 287. The 796 in the dispatch is a grep line count that includes doc comments.

Why the shipped row and not the others:

  • Per-key text in tsc would need every guidance to be one literal. That means joining 154 concatenations into lines of 300 to 600 chars, re-typing 83 constants, and giving 49 helper functions template-literal return types, which is not a mechanical rewrite. The named generic brand also needs a new public type name, and tsc truncates the text anyway. So the per-key prescription stays the parse's and os validate's, and the type names the door.
  • Inline and anonymous, not named: a named type prints by name (the sentence is lost for primitive and object values). It would also appear in the declared type of every tombstoned schema, as a type every consumer's declaration emit must be able to name through an entry point. That means new exports on about 15 entry barrels, outside this card's file surface.
  • The mark rides both sides: the input-only row was implemented first (b8fad3540b). It turned 21 ADR-0122 isomorphism pins in type-alias-convention.pin.test.ts red (TS2344). Each of those schemas differs between z.input and z.infer only by the mark, and ADR-0122 answers that difference with a synonym XParsed alias per schema. With the mark on both sides the pins hold and no alias is needed. Parsed data never holds the key, so the output is as empty as undefined was.

Declared-surface delta

  • api-surface/**, api-surface-signatures.json (factory signature hashes), declaration-map/**, export-origins/**, authorable-surface/**, JSON schemas and content/docs/references/**: 0 bytes. check:generated reports 15 of 15 current at 9525651cf7, and nothing needed regenerating. No export was added or removed. check:entry-nameability is green, because each site's declared type references zod alone.
  • Published declarations (dist/**/*.d.ts + *.d.mts): 27,614,911 → 29,691,599 bytes, +2,076,688 (+7.5%). Base 3693a1b50d, against b9b13c4599 on the same base. Of the 4,084 occurrences of the old z.ZodOptional over z.ZodNever declared type, 20 remain; those are hand-written z.never().optional() sites, not tombstones. The mark is spelled out 4 times per site, 19,180 occurrences in all: once each as input and output, then again inside zod's internals parameter.
  • Consumer tsc --extendedDiagnostics on the defineStack probe, four runs each on a shared box: Lines of Definitions 135,138 → 144,018 (+6.6%), Types 69,736 → 72,753 (+4.3%), memory about 417 MB → about 436 MB (+4.5%). Total time ranged 5.16 to 5.77 s on base and 5.24 to 5.64 s with the change, within noise.
  • check:type-check-debt --re-measure: 53 raw errors, none above its record. check:type-check-coverage is green.

Pins

  • packages/spec/src/shared/retired-key-tsc-diagnostic.test.ts (new) compiles in-memory defineStack probes against the spec source with the compiler API and reads each diagnostic. It covers page assignedProfiles (strictObject, array, TS2741), field conditionalRequired (strictObject, string, TS2322) and index type (non-strict z.object, string, TS2322). Each case asserts exactly one diagnostic, on the retired key's own line, containing [REMOVED] and `os validate`. A control probe, the same stack with no retired keys, must compile with zero diagnostics.
  • Two existing pins asserted the old diagnostic and are updated: data/driver.test.ts (DriverCapabilities' retired bits typed exactly undefined) and integration/connector-author-shape.test.ts (three retired connector keys, TS2322 / not assignable to type 'undefined'). They now assert the tombstone mark.
  • Ablation, run from the committed head 9525651cf7 with node scripts/ablation-replace.mjs. It deletes the return annotation (retiredKey(guidance: string): RetiredKeySchema { → retiredKey(guidance: string) {): anchor 1 → 0, blob 0b3d44fb78eb → f09c06a27cf8. Result: 7 failed and 66 passed across the 3 files. All three new cases went red with L16 TS2322: Type 'string[]' is not assignable to type 'undefined', along with the driver pin and the three connector cases; the control stayed green. Restored: blob equals HEAD (0b3d44fb78eb), git diff HEAD is empty and the tree is clean. The probes resolve @objectstack/spec to src/ through the compiler's own paths, so no dist is involved.

Fixed in place (same defect class, outside the claimed file surface)

  • packages/spec/src/compose-stacks.test.ts: two fixtures wrote the retired App.objects key (objects: ['account']), and nothing in the test asserts it. That ledgered debt changed signature under this change, and check:test-typecheck called it maintainer-only to re-record. The key is deleted per its own prescription ("Delete the key."). test-typecheck-debt.json was re-recorded by gen:test-typecheck-debt as a pure shrink (−1 line: the not assignable to type 'undefined' signature, 2 errors).
  • data/driver.test.ts and integration/connector-author-shape.test.ts, as above.

No open PR touched any of these files at the time of the edit, from a read of all open PRs' file lists.

Verification (head 9525651cf7 unless noted)

  • dispatch-gates --commands: 84 commands, all exit 0, reconciled with --ran: 84 derived, 84 run, 0 NOT-MEASURED, derived from the recorded exit codes.
  • pnpm --filter @objectstack/spec typecheck (src tsc, scripts, test layer): green. check:generated: 15 of 15 current.
  • pnpm --filter @objectstack/spec exec vitest run --project local: 585 files, 17,209 passed, 1 todo.
  • --project repo: 37 of 47 files (608 tests) green in three chunks. The other 10 are NOT MEASURED (see the next section).
  • turbo run build over ./packages/* and ./packages/*/*: 71 of 71, including every downstream DTS emit over the new declarations.
  • Consumer lanes at 9112064873, which has the same type semantics (the later commits are a comment rewrap and a merge of non-spec main): pnpm --filter @objectstack/downstream-contract run typecheck, pnpm --filter './examples/*' run typecheck (5 apps) and turbo run typecheck over ./packages/*, ./packages/*/* and ./apps/* (135 of 135 tasks) are all green.
  • eslint, narrowed to the 5 touched .ts files: --format json reports 5 files, 0 errors, 0 warnings. The repo has one eslint.config.mjs, and its files glob covers every **/*.ts here. That config never enables type-aware linting (the resolved parserOptions is { ecmaVersion, sourceType }), so this diff cannot move any untouched file's verdict. The full pnpm lint is CI's.

NOT MEASURED

  • 10 spec --project repo tooling tests that spawn builds or merges (build-schemas-check-mode, dist-freshness-adoption, dist-freshness, conversions-major18-merge, count-shards-merge, step18-rationale-merge, def-key-collisions, sharded-artifacts, publish-smoke-boot-failure, publish-smoke-port-collision). Reason: the whole project ran past the ~10-minute foreground cap twice. None of them reads retired-key typing. CI runs test:repo.
  • objectui typecheck against the new declarations. Reason: objectui consumes the published spec, so this reaches it at its next spec bump, and the Console Pin Gate build bundles with vite and does not type-check.

Acceptance notes

  • About a dozen spec tests carry @ts-expect-error comments that say the tombstone's "input type is never" (it was undefined, and it is now the mark). The expectations still fire; only the comment text drifted. Carrier: none.
  • A consumer that reads a tombstoned key into a slot typed exactly undefined, or into a typed slot such as string[] | undefined, no longer compiles. The key never holds a value, so the fix is to delete the dead read. The changeset says so. In-repo population: zero, per the workspace and consumer typecheck lanes above.
  • The dispatch said to stop and explain for any file outside the claimed surface. I applied os-dev's bounded in-place fix instead, because all four conditions hold: same defect class, a mechanical form pinned by the retirement's own prescription and by the new pin, no other holder of the files, and the same gate family. The conflict is recorded here and in the report.

Generated by Claude Code

retiredKey() now declares its input type as an object whose one required
property is named by the retirement sentence and typed never, so writing a
tombstoned key prints that the key was removed from @objectstack/spec and
that os validate prints its migration, instead of a bare 'not assignable to
type undefined'. The runtime schema is unchanged (an upcast of the same
z.never().optional()).

Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6
Co-authored-by: Claude <noreply@anthropic.com>
…d names os validate

Compiles in-memory defineStack probes against the spec source and reads the
diagnostics: page.assignedProfiles (strictObject, array), a field's
conditionalRequired (strictObject, string) and an index's type (non-strict
z.object, string), with a clean control probe for anti-vacuity.

compose-stacks.test.ts authored the retired App.objects key in two fixtures
that assert nothing about it; the key is deleted per its own prescription,
which retires the two test-typecheck debt entries whose signature this change
rewrote.

Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6
Co-authored-by: Claude <noreply@anthropic.com>
…sm holds

A mark on the input alone split 21 otherwise-isomorphic schemas pinned in
type-alias-convention.pin.test.ts into two shapes that differ only by the
diagnostic device, which ADR-0122 would answer with a synonym XParsed each.
The output now carries the same uninhabitable mark. The sentence is shorter
because the declaration emitter spells the mark out at every site.

Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6
Co-authored-by: Claude <noreply@anthropic.com>
…undefined'

DriverCapabilities' retired bits and the connector example's three retired
keys pinned the old diagnostic, which named no retirement. They now assert
the tombstone mark ([REMOVED], os validate) that retiredKey() declares.

Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Oct 1, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/test-typecheck-debt.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/test-typecheck-debt.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2f2fa11d756f665a4c06160480c1dce15b9d67a4 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from faf68f4f9ba0fabf6e393b911694aaf15f3c6079 — the merge of head 0e71b33e4f74f053c1cdfcaeb1debb2d5132ca97 into base 2f2fa11d756f665a4c06160480c1dce15b9d67a4, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin faf68f4f9ba0fabf6e393b911694aaf15f3c6079 && git checkout faf68f4f9ba0fabf6e393b911694aaf15f3c6079
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2f2fa11d756f665a4c06160480c1dce15b9d67a4 0e71b33e4f74f053c1cdfcaeb1debb2d5132ca97 && git checkout -B drift-repro 2f2fa11d756f665a4c06160480c1dce15b9d67a4 && git merge --no-ff 0e71b33e4f74f053c1cdfcaeb1debb2d5132ca97

node scripts/docs-audit/affected-docs.mjs --json 2f2fa11d756f665a4c06160480c1dce15b9d67a4

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9525651cf770ff82cf167626dc29898ef89ccd57
Local-runs: none

Inputs read: card #20621 (body; comments 5888072973, 5921046652, 5922075855, 5923368364), PR #21023 (body, 7-file list, net diff of refs/review/pr21023 against main at 2f2fa11d75, merge-base 7fa67dada3), and the 35 check-runs on the head. Nothing was built, run or re-run; the census below is a read-only parse of git show output at the head.

Check-runs on the head, read at 2026-10-01T02:32Z: all 35 completed. Every gate-carrying job reads success, the seven required contexts included (Lint & Repo Gates, TypeScript Type Check, Test Core 6/6 shards, Dogfood Regression Gate 3/3, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard), plus Type Check · source gates / debt ledger / consumer gates / workspace, Spec property liveness, Check Changeset. Three read skipped by design: Console Pin Gate (paths filter on .objectui-sha and console build inputs, none touched), Build Docs, Packed-tarball smoke (opt-in).

① Derived judgments

  • The declared type, right. retiredKey() now returns RetiredKeySchema, spelled as z.ZodOptional over z.ZodType whose Output and Input are both Mark, where Mark is an object type with one required property named by the retirement sentence and typed never. A required never property is satisfiable by no object literal, no index-signature type and no {}; only never, any or a cast reach it. So z.input and z.infer of a tombstoned key both read Mark | undefined and admit absence alone, on input and output alike. Uninhabited: confirmed.
  • The upcast, sound. The body is byte-identical: z.never({ error }).optional().describe('[REMOVED] …'), so the runtime value is ZodOptional over ZodNever. zod 4.6.1 declares ZodType with out variance on Output, Input and Internals, and ZodOptional.unwrap() returns T in a covariant position, so ZodNever upcasts to ZodType of Mark, Mark without a cast. The head's Type Check · source gates and · workspace are green on exactly that assignment.
  • No in-tree reader typed on ZodNever. git grep ZodNever over packages/** on main returns nothing, in source or tests. Every .unwrap() call on main is on a live key (limit, retryPolicy, droppedFields, …), none on a tombstone. acceptsNothing (packages/metadata-protocol/src/unauthorable-nodes.ts:99) reads JSON Schema (not: {}) at runtime, not the TS type. sql-driver.ts:5511 strips windowFunctions with Omit before re-declaring it, so the mark never reaches SqlWindowFunctionQuery (its comment "resolves to undefined" is now stale, comment-only). acceptRetiredDefaultResidue and enumWithRetiredValues do not depend on retiredKey()'s return type.
  • Triage's "runtime unchanged" holds. The parse, the prescription string, the .describe() text, the ADR-0087 conversions and every _zod.def walker see what they saw. The generated artifacts (api-surface/**, authorable-surface/**, JSON schemas, content/docs/references/**) move 0 bytes; the spec generated-artifact gates ran green in the head's TypeScript Type Check family. Note the limit of that evidence: check:api-surface records that an export exists, never what its type resolves to, so this declared-type change is invisible to the gate family by construction; this review is the control.
  • ADR-0122 isomorphism, right. type-alias-convention.pin.test.ts asserts Eq of z.input and z.infer for 778 aliases that deliberately carry no XParsed. A mark on the input side alone makes every tombstoned member of that list non-isomorphic (the dev measured 21 TS2344), and ADR-0122's only answer to a genuine input/output difference is a new public XParsed alias per schema. Carrying the same mark on both sides keeps the pins true and adds no name. Right, and forced.
  • Census correction, confirmed exactly. A read-only parse of every non-test .ts under packages/spec/src at the head, comments stripped: 287 retiredKey() calls in 72 files; 0 pass a single literal; 155 pass a '…' + '…' concatenation (the dev's 154 + 1 with a non-literal operand), 83 a constant, 49 a helper's return. The dispatch's 796 is the raw grep line count including doc comments; 80 further hits are the string retiredKey() inside ADR-0087 migration entries. TypeScript types all 287 arguments as string, so triage's "generic over the guidance literal" would carry text at no site. The rejection of that design is well-founded.
  • File-surface deviation, each forced and minimal. compose-stacks.test.ts: two fixtures wrote the retired App.objects; their ledgered signature ("not assignable to type 'undefined'", 2) vanishes under this change, and check-test-typecheck.mts reds a file whose signature arrives or vanishes until re-recorded, so an edit was unavoidable; deleting the key per its own prescription is the 2-line form. test-typecheck-debt.json: regenerated as a pure shrink (one signature line removed), the shrink-only direction the ledger allows. data/driver.test.ts and integration/connector-author-shape.test.ts pinned the literal old diagnostic ('undefined', TS2322) and would have gone red; the edits re-pin the mark, plus one added negative assertion that live bits are not tombstones in driver.test.ts (a small strengthening, acceptable). Ablation evidence in the PR body (annotation removed, 7 failed / 66 passed, control green) matches the pins' claimed reach.
  • d.ts growth, within the gates. Published declarations +2,076,688 bytes (+7.5%), from the emitter spelling the anonymous mark at every site. On the head: Type Check · debt ledger green (53 raw errors, none above record), Type Check · consumer gates green, Type Check · workspace green, Build Core green (71/71 DTS emits). No ratchet moved; the measured consumer tsc cost (+4.3% types, +4.5% memory, time within noise) is consistent with those gates. Acceptable; the inline-anonymous choice is the one that adds no public name.
  • NOT MEASURED items. (a) The 10 spawn-heavy spec --project repo tests: CI's Test Core job runs pnpm turbo run test test:repo (ci.yml:755), so they ran on this head; all six shards and the Test Core rollup read success. Covered. (b) objectui against the new d.ts: Console Pin Gate is paths-filtered off this PR and bundles with vite, so it would not type-check in any case. A read-only grep of objectui origin/main (e420df310f) finds three spec-derived reads of tombstoned keys, all in packages/core/src/actions/ActionRunner.ts: aria, shortcut, bulkEnabled declared as SpecActionInput['…'] (lines 411, 472, 480). Each is a type re-declaration only; ActionEngine.registerAction builds its record field by field from options, never by spreading the ActionDef, so nothing reads them into a boolean/string slot. objectui's own undefined pins (AppMenuItem['shortcut'], PageNodeSchema['assignedProfiles']) are on objectui-local types, unaffected; two tests cite the old diagnostic text in comments only. The ...page spread in PageView.tsx is over any. Risk at objectui's next spec install: low, confined to its ActionDef d.ts carrying the mark on three members and to comment drift; no compile break found.

② Semver level

  • '@objectstack/spec': minor is the right bump under the lockstep fixed group and the launch-window guard (check-changeset-no-major.mjs: breaking ships as minor; a major is refused).
  • Clause-②: yes with no arm is admissible: the clause asks whether the card widens an accept set or enlarges the public surface, and references/lanes/spec.md rules that declaring yes is never an error. No accept set moves in either direction: at the runtime the key is refused exactly as before, and at tsc absence was and remains the only accepted authoring. So no (narrowing) arm is owed.
  • What is wrong: the changeset declares no break while documenting one. Its "Who might notice" paragraph states that consumer code reading a tombstoned key into a slot typed undefined, or into a typed slot such as string[] | undefined, "no longer compiles". That is a published declared-type change (the dev counts 4,084 declaration sites moving from ZodOptional over ZodNever) after which a previously valid consumer program is invalid; that the broken read is dead by construction does not make it compile. This repo's own doctrine on exactly this class (check-adr-0087-registration.mjs, the type-surface-only section) is that a published type-surface change a consumer can stop compiling on "declares **BREAKING** truthfully", and that nudging the class away from the marker is the erosion the category was built to stop; precedent .changeset/20648-sys-migration-flag-point-lookup.md declares **BREAKING** for a population as marginal as "the TypeScript author of a hand-written stand-in". AGENTS.md then requires a breaking changeset to state its ADR-0087 disposition in writing. The changeset carries neither carrier, so the gate (which judges only declared breaks) was silent, and an upgrading consumer whose typecheck goes red after this minor will not find this entry by grepping CHANGELOG.md for BREAKING.
  • What the next head owes, two lines in the changeset body, nothing in code: (1) a **BREAKING** banner line stating the one compile break (a read of a tombstoned key into a slot typed undefined or with the key's old type no longer compiles; delete the dead read; shipped as minor under the launch-window convention); (2) one ADR-0087 disposition as an HTML-comment marker reading adr-0087: not-required (no-migration-prescription) with the why: no authorable key, spelling, export or stored shape moves, every tombstoned key's own retirement is already ledgered, objectstack migrate meta has nothing to rewrite, and the affected party is a TypeScript consumer whose fix the compiler delivers at the call site. That category is open to this body: it carries no arrow rewrite, no migration table and no migration heading, so the prescription detector does not refuse it; type-surface-only is closed (its predicate 2 refuses any diff touching packages/spec), runtime-interface-only is closed (retiredKey is referenced in code at 287 sites), registered / already-registered are closed (no new id; the existing ids cover the key retirements, not this type mark). Clause-②: yes stays as written.

③ Boundary flags

  • Dev deviation 1 (three test files and the debt ledger outside the claimed surface): judged under os-dev.md's bounded in-place-fix exemption, all four conditions holding on the evidence above (same defect class; mechanical form pinned by the retirement's prescription and the new pin; No other open PR may claim the same single-writer path green on the head; same gate family, no new verification surface). The conflict with the dispatch's "stop and explain" is answered: os-dev.md governs, and the dev reported it in both places. What the exemption still owes is the seat's half: the claim's declared file surface is to be amended in the same round. Escalated to the dispatching seat (domain:spec#1), not a verdict ground.
  • Dev deviation 2 (design differs from triage's literal-generic example): answered by the census, confirmed above. Right.
  • Dev deviation 3 (mark on the output side): forced by the ADR-0122 pins, confirmed above. Right.
  • Dev deviation 4 (287 not 796): confirmed above.
  • Dev deviations 5 and 6 (model-free commit trailers; two os-regen-merge.sh merges with nothing to regenerate): consistent with AGENTS.md; nothing owed.
  • open_questions: none declared; none found.
  • alternatives_measured (per-key text in tsc; +7.5% d.ts): the shipped choices A and A are the right ones for the reasons in ①; no maintainer decision needed.
  • out_of_scope_findings (comment drift: about a dozen spec @ts-expect-error comments say the tombstone input is never; add packages/drivers/driver-sql/src/sql-driver.ts:5511 saying it resolves to undefined): carrier none, as the dev graded; outside the three filing classes. Noted.
  • No governed surface in the file list; Governed Surface Queue Guard green. The PR is a draft, auto-merge not armed; nothing to disarm.

Implemented-by: claude/issue-20621-retired-key-type-names-retirement
Reviewed-by: session_018fxqvRJW12TaHC7DUQ89Y6

VERDICT: FAIL

Verdict basis: ① and ③ hold in full; ② fails on one declaration defect with a two-line fix in .changeset/20621-retired-key-tsc-names-retirement.md (the **BREAKING** banner and the ADR-0087 not-required (no-migration-prescription) marker). No code change is asked for; a re-review at the next head re-reads only the changeset and the check-runs.

claude added 2 commits October 1, 2026 02:38
A read of a tombstoned key into a slot typed undefined, or typed with the
key's old type, no longer compiles. The changeset now says so with a
BREAKING banner, and states not-required (no-migration-prescription).

Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0e71b33e4f74f053c1cdfcaeb1debb2d5132ca97
Local-runs: none

This record answers the FAIL record 5923591450 rendered on head 9525651cf7, whose only failing section was ②. Inputs this round: PR #21023 at its current head (body, 7-file list, net diff of refs/review/pr21023 against main at 2f2fa11d75, which is now also the merge-base), the changeset at that head, the two commits since 9525651cf7, and the check-runs on the head. Nothing was built, run or re-run; the census figure below is a read-only parse of git show output.

Delta, verified. Two commits: f26e7abffd (changeset only, +4 lines) and the merge 0e71b33e4f (parents f26e7abffd and 2f2fa11d75, no rebase). Per-file blobs against the round-0 head: compose-stacks.test.ts 5d627cc3fe, data/driver.test.ts ceb41bb9c2, integration/connector-author-shape.test.ts eafdce8fca, shared/retired-key-tsc-diagnostic.test.ts c8af6fa59d, shared/retired-key.ts 0b3d44fb78, test-typecheck-debt.json 23abb9a04a — all six identical; the changeset moved 914ba4a0e8 → 4b37bff562. The merge's diff against its first parent restricted to the seven PR files is empty, and the net file list against main is identical to round 0. So the net diff is the round-0 diff plus the changeset delta and nothing else; no code change. main's own delta between the two merge-bases (7fa67dada3..2f2fa11d75) touched none of packages/spec/src/shared/, the ledger, or the three gate scripts ② relies on (check-adr-0087-registration.mjs, pm/clause2-line.mjs, check-changeset-no-major.mjs); its one touch near this change wraps component.zod.ts's body tombstone as retiredComponentSlot(retiredKey(…)), where retiredComponentSlot is a generic identity over z.ZodType that registers the instance in a map and reads nothing from the type. The census at this head is unchanged: 287 calls in 72 files, 0 literal.

Check-runs on the head, read at 2026-10-01T02:54Z: 34 check-runs, none concluded other than success or skipped. Completed success: Lint & Repo Gates, TypeScript Type Check (with Type Check · source gates / debt ledger / consumer gates / workspace), Build Core, Dogfood Regression Gate 3/3, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard, Check Changeset, Spec property liveness, Test Core shards 2/6, 3/6, 4/6, and the claim/closing guards. Still in_progress at that read: Test Core shards 1/6, 5/6, 6/6 (and so the Test Core rollup). Skipped by design: Console Pin Gate (paths filter), Build Docs, Packed-tarball smoke (opt-in). Round 0's run of the same jobs on a tree whose six source blobs are byte-identical was green throughout; the seat reads the final conclusions before queueing, as Prime Directive #14 requires.

① Derived judgments

Carried forward by reference from 5923591450, unchanged: every judgment there was rendered on the six source blobs that this head carries byte-identically (the uninhabited mark on input and output, the sound upcast under zod 4.6.1's out variance, no in-tree reader typed on ZodNever, runtime unchanged with 0-byte generated artifacts, the ADR-0122 isomorphism argument, the 287/72/0 census, the forced and minimal test-file edits, the d.ts growth within the type-check gates, CI covering the ten spawn-heavy tests, and the low objectui exposure). The delta adds one in-tree touch point, retiredComponentSlot, an identity wrapper that imposes no reader on the declared type; the head's Type Check · source gates and · workspace are green on it.

② Semver level

  • The **BREAKING** banner is present and truthful. It names the one compile break (a read of a tombstoned key into a slot typed undefined, or typed with the key's old type, no longer compiles, because the declared type is now the tombstone mark), the fix (the key never holds a value; delete the dead read), and the bump (minor under the launch-window convention). That matches what the diff publishes and the wording of precedent (.changeset/20648-*, 20745-*). It is signal (2) of breakingDeclaration, so the gate now judges this changeset; it is absent on main and therefore judged as a newly added breaking declaration at merge-base 2f2fa11d75.
  • The ADR-0087 marker is present, in the gate's grammar, and its category is the right one. An HTML-comment marker reading adr-0087: not-required (no-migration-prescription) followed by the why. Its load-bearing facts are accurate: only the declared TypeScript type of each tombstone moves, on both sides; no authorable key, spelling, export or stored shape moves; this diff retires nothing new, so objectstack migrate meta has nothing to rewrite; the body carries no migration prescription (no arrow rewrite, no table, no migration heading, so the detector does not refuse it); the affected party is a TypeScript consumer shown the dead read by the compiler. It closes the other categories on facts that hold: the package publishes (not unpublished); no id covers this type mark and none is added (not registered / already-registered); retiredKey is referenced in code at 287 sites (not runtime-interface-only); the diff touches packages/spec (not type-surface-only, whose predicate 2 refuses a spec diff). One supporting sentence is overbroad and should be read as such: "every tombstoned key's own retirement is already ledgered under its own ADR-0087 id". Spot-checked at this head: assignedProfiles, conditionalRequired, triggerPhrases, bulkEnabled, shortcut and windowFunctions each have migration entries; homePageId (ui/app.zod.ts:1575) has none under any spelling. The disposition does not rest on that sentence — the category's test is the prescription-free body and that nothing is newly retired — so this is a precision note for a later touch ("where a migration was owed"), not a defect, and not a verdict ground.
  • Clause-②: yes and '@objectstack/spec': minor stand, for the round-0 reasons: no accept set moves in either direction, declaring yes is sanctioned for the spec lane, and the lockstep fixed group ships a declared break as minor under the launch-window guard.
  • Gate verdict on the head. Check Changeset (pr-automation.yml job changeset-check, which runs check-empty-changeset.mjs, check-adr-0087-registration.mjs --base MERGE_BASE and check-changeset-no-major.mjs) reads success at the read above. So the ADR-0087 registration gate judged the newly breaking changeset, accepted the no-migration-prescription disposition, and its prescription detector did not refuse the body; the no-major guard accepted minor. The round-0 defect is closed.

③ Boundary flags

Carried forward by reference from 5923591450, unchanged. The seat's half of the bounded in-place-fix exemption — amending the claim's declared file surface on card #20621 — remains the dispatching seat's (domain:spec#1), not a verdict ground. No new deviation arrives with this delta: f26e7abffd is the changeset edit this review asked for and nothing else, and the merge came through os-regen-merge.sh with nothing to regenerate. No governed surface in the file list; Governed Surface Queue Guard green; the PR is a draft with auto-merge not armed. No open_questions.

Implemented-by: claude/issue-20621-retired-key-type-names-retirement
Reviewed-by: session_018fxqvRJW12TaHC7DUQ89Y6

VERDICT: PASS

Verdict basis: ② is now right — the banner and the ADR-0087 disposition the round-0 record asked for are present, truthful and gate-accepted; ① and ③ hold unchanged on a delta that is the changeset alone. Landing remains conditional on every check reading green at the seat's own read, per Prime Directive #14; three Test Core shards were still in progress at this record's read.

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 protocol:data size/m tests tooling

Projects

None yet

2 participants