Repository navigation
feat(spec): a retired key's tsc error names the retirement and points at os validate (#20621) - #21023
Conversation
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>
…es retired Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
…tirement Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
…tired-key-type-names-retirement
…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>
Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
…tired-key-type-names-retirement
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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 |
Contract reviewServed-tier: Inputs read: card #20621 (body; comments 5888072973, 5921046652, 5922075855, 5923368364), PR #21023 (body, 7-file list, net diff of Check-runs on the head, read at 2026-10-01T02:32Z: all 35 completed. Every gate-carrying job reads ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Verdict basis: ① and ③ hold in full; ② fails on one declaration defect with a two-line fix in |
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>
…tired-key-type-names-retirement
Contract reviewServed-tier: This record answers the FAIL record 5923591450 rendered on head Delta, verified. Two commits: Check-runs on the head, read at 2026-10-01T02:54Z: 34 check-runs, none concluded other than ① Derived judgmentsCarried 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 ② Semver level
③ Boundary flagsCarried 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 ( Implemented-by: 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 |
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, typednever, whose name is the retirement sentence. Writing the key now failstscwith an error that names the retirement, for every kind of value: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, becauseneveris assignable to the mark. The parse error, its prescription, the textos validateprints, the D2 conversions,acceptsNothingand the JSON-schema/authorable-surface walkers all see what they saw before. No value can have the mark's type without a cast, sotscstill accepts only absence.Measured channel table
Probes: a scratch
tscproject per design over zod directly, plus the real built@objectstack/specdist with adefineStackstack. The stack carries a page'sassignedProfiles(PageSchema, astrictObject, array value), a field'sconditionalRequired(FieldSchema, astrictObject, string value, a rename retirement) and an index'stype(IndexSchema, a non-strictz.object, string value). Strict and non-strict printed identically in every design.strictObjectvsz.objecttscchannelundefinednot assignable to type 'undefined'key?: undefined✗RetiredKeyGwith the text inside its type argument, truncated by tsc at about 330 charsRetiredKeyGofstring: no text, and thestringargument reads as "wants a string"RetiredKeyRetiredKey(the sentence prints only for array values, if it is the property name)key?: RetiredKey~[REMOVED]followed by Gstring; it admits any string with that prefix, which the parse refuses, and steers a string author toward writing onetscundefined, orundefinedintersected with a brandundefined@deprecatedJSDoctscnever prints it (a suggestion diagnostic only; 6385 did not fire on an object-literal property in the probe)z.input-annotated literal and insidedefineStackobjectsCall-site census: 287
retiredKey()calls in 72 files ofpackages/spec/srcat9525651cf7. 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 argumentstringat all 287. The 796 in the dispatch is a grep line count that includes doc comments.Why the shipped row and not the others:
tscwould 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 andos validate's, and the type names the door.b8fad3540b). It turned 21 ADR-0122 isomorphism pins intype-alias-convention.pin.test.tsred (TS2344). Each of those schemas differs betweenz.inputandz.inferonly by the mark, and ADR-0122 answers that difference with a synonymXParsedalias 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 asundefinedwas.Declared-surface delta
api-surface/**,api-surface-signatures.json(factory signature hashes),declaration-map/**,export-origins/**,authorable-surface/**, JSON schemas andcontent/docs/references/**: 0 bytes.check:generatedreports 15 of 15 current at9525651cf7, and nothing needed regenerating. No export was added or removed.check:entry-nameabilityis green, because each site's declared type referenceszodalone.dist/**/*.d.ts+*.d.mts): 27,614,911 → 29,691,599 bytes, +2,076,688 (+7.5%). Base3693a1b50d, againstb9b13c4599on the same base. Of the 4,084 occurrences of the oldz.ZodOptionaloverz.ZodNeverdeclared type, 20 remain; those are hand-writtenz.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.tsc --extendedDiagnosticson thedefineStackprobe, 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-coverageis green.Pins
packages/spec/src/shared/retired-key-tsc-diagnostic.test.ts(new) compiles in-memorydefineStackprobes against the spec source with the compiler API and reads each diagnostic. It covers pageassignedProfiles(strictObject, array, TS2741), fieldconditionalRequired(strictObject, string, TS2322) and indextype(non-strictz.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.data/driver.test.ts(DriverCapabilities' retired bits typed exactlyundefined) andintegration/connector-author-shape.test.ts(three retired connector keys,TS2322/not assignable to type 'undefined'). They now assert the tombstone mark.9525651cf7withnode scripts/ablation-replace.mjs. It deletes the return annotation (retiredKey(guidance: string): RetiredKeySchema {→retiredKey(guidance: string) {): anchor 1 → 0, blob0b3d44fb78eb→f09c06a27cf8. Result: 7 failed and 66 passed across the 3 files. All three new cases went red withL16 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 HEADis empty and the tree is clean. The probes resolve@objectstack/spectosrc/through the compiler's ownpaths, 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 retiredApp.objectskey (objects: ['account']), and nothing in the test asserts it. That ledgered debt changed signature under this change, andcheck:test-typecheckcalled it maintainer-only to re-record. The key is deleted per its own prescription ("Delete the key.").test-typecheck-debt.jsonwas re-recorded bygen:test-typecheck-debtas a pure shrink (−1 line: thenot assignable to type 'undefined'signature, 2 errors).data/driver.test.tsandintegration/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
9525651cf7unless 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(srctsc, 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 buildover./packages/*and./packages/*/*: 71 of 71, including every downstream DTS emit over the new declarations.9112064873, which has the same type semantics (the later commits are a comment rewrap and a merge of non-specmain):pnpm --filter @objectstack/downstream-contract run typecheck,pnpm --filter './examples/*' run typecheck(5 apps) andturbo run typecheckover./packages/*,./packages/*/*and./apps/*(135 of 135 tasks) are all green..tsfiles:--format jsonreports 5 files, 0 errors, 0 warnings. The repo has oneeslint.config.mjs, and itsfilesglob covers every**/*.tshere. That config never enables type-aware linting (the resolvedparserOptionsis{ ecmaVersion, sourceType }), so this diff cannot move any untouched file's verdict. The fullpnpm lintis CI's.NOT MEASURED
--project repotooling 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 runstest:repo.Acceptance notes
@ts-expect-errorcomments that say the tombstone's "input type isnever" (it wasundefined, and it is now the mark). The expectations still fire; only the comment text drifted. Carrier: none.undefined, or into a typed slot such asstring[] | 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.Generated by Claude Code