You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding][devx] a retired spec key surfaces in tsc as TS2322: Type 'string' is not assignable to type 'undefined' — nothing in the type error says the key was retired or what replaces it #20621
Observed on a real upgrade: HotCRM 17.4.0 → 17.5.0, 2026-09-29
After the bump, tsc (hotcrm pnpm typecheck) reported the retired keys like this:
src/sales/pages/home.page.ts(342,3): error TS2322: Type 'string[]' is not assignable to type 'undefined'.
This covered page.assignedProfiles (6 sites) and chartConfig.{type,xAxis,yAxis} (34 sites). Only os validate / defineStack explain what happened ("… was removed in @objectstack/spec 17.5.0 … Delete the key …").
A developer who only watches their editor or tsc cannot tell that this is a retirement with a documented migration. It reads like a typing bug.
Ask
Make the type-level tombstone carry the retirement, for example:
a branded never whose name states the retirement;
@deprecated JSDoc on the tombstoned property, carrying the same prescription retiredKey() already holds.
The goal is that the editor hover and the tsc message point to the migration.
Path: the road's upgrade step — the author's own compiler says a key was retired and what replaces it | 缺项 (retiredKey() types the key's input as undefined, so tsc prints TS2322 with no retirement and no prescription) | P3
Triage: first grade — tooling · priority:p3 · domain:spec (rerouted from domain:devx) · area:devpath · pm:queue. Direction: the type-level tombstone carries the prescription retiredKey() already holds
Triage: lands in packages/spec/src/shared/retired-key.ts ⇒ domain:spec. The lane table puts packages/spec/** there, whoever reads the result. The shipped surface is the @objectstack/spec types an author compiles against, so tooling stays.
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-29T10:08Z. ⛔ Not a claim, ⛔ not a dispatch.
The mechanism.
retiredKey(guidance) (retired-key.ts:113) returns z.never({ error: () => guidance }).optional(). z.input of that is undefined, so a tsc author sees Type 'string[]' is not assignable to type 'undefined'.
The file's own header (:34) lists tsc as one of the two channels that make a removal audible. It is audible, but it says nothing about what happened or what to do. Only os validate / defineStack carry the prescription.
Why p3. A tax on the upgrade step, not a wall. The key is refused loudly on every channel, and os validate names the migration. The cost is an author reading 40 type errors as a typing bug; HotCRM's upgrade measured 6 + 34 such sites.
Direction.
The input type names the retirement. For example, make retiredKey generic over its guidance literal, so the input type is a brand carrying the text. tsc then prints the prescription inside the "not assignable to" message.
Or @deprecated. Add the JSDoc if it measurably reaches the editor hover through z.input. Measure both channels before choosing. ⛔ Don't ship a hover-only fix, because the tsc message is the channel the card measured.
⛔ Runtime unchanged. The parse error, os validate's text and the D2 conversions don't move.
Declared types. Every tombstoned schema's .d.ts changes, so run the api-surface gate. It is a type-only narrowing of a key that is already refused.
Pins:
A type-test that authors a retired key (for example a page's assignedProfiles) and asserts the tsc diagnostic contains the retirement text.
The same test on a strictObject tombstone.
Related, not a duplicate:#20465 asks which route a .strict() retirement takes. If it rules for retiredKey() everywhere, this card's reach grows, but the two don't serialize.
Claim: PM loop round 5 · 2026-09-30T22:47Z
Session: session_018fxqvRJW12TaHC7DUQ89Y6
Account: os-warren (the seat's linked user as GET /user answers it; the card's assignee from this act)
Branch: claude/issue-20621-retired-key-type-names-retirement
Worktree: objectstack-issue-20621
Domain: domain:spec
Seat: domain:spec#1 (seat post #6017)
File surface:
packages/spec/src/shared/retired-key.ts (retiredKey, :113 at 013f97df93, and its type);
a type-test beside it, plus the strictObject tombstone case triage names;
the declared-type artifacts the spec build regenerates for tombstoned schemas (api-surface/**, declaration-map/** and whatever check:generated names), by their generators only;
one .changeset/20621-*.md.
⛔ The 796 retiredKey( call sites are not rewritten by hand unless the measured design needs it, and then only by a mechanical, reported transform. Stop on breach; explain in the report.
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier at 013f97df93: no path-derived mandate; floor sonnet · default opus · ceiling fable). It is a type-level design with a measured fork, so the default tier. The at-tier ## Contract review is owed: the declared input type of every tombstoned key changes.
Clause-②: yes
Thread-read: 5888072973
Serial constraints cleared:
Provenance for dispatching a tooling card while the fleet tooling slot is held (#20765): the maintainer, in this session's chat: 「工具卡的全舰队名额仍被 #20765 占着,p1 是优先,但p1已经在开发中时,不应该卡住其他卡的派发」. A card already in development does not hold other cards' dispatch.
Why Clause-②: yes. The runtime accept set does not move: the key is still refused, with the same parse error, os validate text and D2 conversions (triage's ⛔). What changes is the published .d.ts input type of every tombstoned key, today undefined. Triage calls it "a type-only narrowing". It is graded yes so the public-surface change gets its at-tier review.
The fork the dev measures first: many retiredKey() guidances are built by '…' + '…' concatenation. TypeScript types those as string, not as a literal, so "generic over the guidance literal" may carry no text at those sites. Both channels triage names (the tsc message, and @deprecated through z.input) are measured before any choice. ⛔ No hover-only fix.
Audit note · domain:spec seat 2 (session_017VaLJnYwhPsanVCe9dMCJU) · 2026-10-01T00:15Z · ⛔ Not a claim.
This seat's step-1 label write added os-bill as a second assignee here at 2026-10-01T00:14Z, although seat 1's claim 5921046652 already held the card. It was the seat's own error: the pre-read showed the claim, and the batch write ran anyway. It was rolled back at 2026-10-01T00:15Z: the assignee is os-warren alone again and the labels are unchanged. No claim was posted and no dev was dispatched. Seat 1's claim stands.
{
"issue": 20621,
"status": "done",
"branch": "claude/issue-20621-retired-key-type-names-retirement",
"pr": "https://github.com/objectstack-ai/objectstack/pull/21023",
"session": "session_018fxqvRJW12TaHC7DUQ89Y6 (the parent's harness-stamped id; this run is a subagent of it)",
"premise_still_valid": true,
"summary": "The card's premise held: at base 3693a1b50d a defineStack page's assignedProfiles printed TS2322 'not assignable to type undefined'. retiredKey() now declares an explicit return type: both its input and its output are an inline anonymous object type with one property, typed never and named '[REMOVED] Key retired: run `os validate` for its migration.'. tsc therefore prints the sentence for every value kind (TS2741 array, TS2322 string, TS2353 object), on strictObject and non-strict z.object alike, and hover shows it too. The runtime is the same z.never().optional(), an upcast with no cast. Triage's mechanism assumption was measured false: 0 of the 287 retiredKey() calls pass a literal (154 concat, 83 constants, 49 helper returns, 1 mixed; the checker types all 287 as string), so a guidance-literal generic carries the text nowhere. A fixed sentence that names the door (os validate) is the shipped floor. The mark also rides the output side, because input-only turned 21 ADR-0122 isomorphism pins red. Generated artifacts move 0 bytes; published d.ts grows +7.5%.",
"tests": "All at head 9525651cf7 unless noted. NEW pin packages/spec/src/shared/retired-key-tsc-diagnostic.test.ts: 4 passed (control clean; page assignedProfiles TS2741, field conditionalRequired TS2322, index type TS2322; each exactly one diagnostic on the key's line containing [REMOVED] and `os validate`). Ablation via scripts/ablation-replace.mjs from committed HEAD: anchor 1 -> 0, blob 0b3d44fb78eb -> f09c06a27cf8, then 7 failed / 66 passed across the new pin + driver.test.ts + connector-author-shape.test.ts (control green, e.g. 'L16 TS2322: Type string[] is not assignable to type undefined'); restore: blob == HEAD 0b3d44fb78eb, git diff HEAD empty, status clean. No dist involved (the probes map @objectstack/spec to src via compiler paths). Input-only variant b8fad3540b measured red: 21 TS2344 in type-alias-convention.pin.test.ts. Suites: spec --project local 585 files / 17209 passed / 1 todo; spec --project repo 37 of 47 files / 608 tests green in 3 chunks (10 spawn-heavy tooling files NOT MEASURED: the whole project ran past the ~10 min foreground cap twice); pnpm --filter @objectstack/spec typecheck green (src tsc + scripts + test layer, 52 files / 246 errors / 135 signatures held); check:generated 15/15 current; turbo build ./packages/* + ./packages/*/* 71/71 at 9525651cf7. Consumer lanes at 9112064873 (same type semantics; later commits are a comment rewrap and a non-spec main merge): downstream-contract typecheck, examples/* typecheck (5 apps), turbo typecheck ./packages/* ./packages/*/* ./apps/* 135/135 tasks. eslint narrowed: 5 touched .ts files, --format json 5 files 0 errors 0 warnings; the single eslint.config.mjs covers **/*.ts and never enables type-aware linting (resolved parserOptions = ecmaVersion, sourceType), so no untouched file's verdict can move. Real-object probe on built dist: base prints 4x 'not assignable to type undefined', new prints the mark in all 4 (TS2322 x2, TS2741 x2).",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 9525651cf7: 84 commands, each exit captured to a file before any pipe; 84/84 exit 0. --ran reconciliation: 84 derived, 84 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero from recorded exit codes). Four of them (doc-formula-expressions, dual-build-cjs-loads, lean-entry-closure, type-check-debt) first answered exit 3 PREREQUISITE NOT MET; after the full packages build they answered 0 (type-check-debt: 53 raw errors, none above record). NOT MEASURED: the 10 spawn-heavy spec repo-project tests named above; objectui typecheck against the new d.ts (objectui consumes the published spec; the Console Pin Gate bundles with vite, no tsc); the CI-only steps dispatch-gates lists outside its 84.",
"line_budget": "n/a",
"files_changed": [
".changeset/20621-retired-key-tsc-names-retirement.md",
"packages/spec/src/shared/retired-key.ts",
"packages/spec/src/shared/retired-key-tsc-diagnostic.test.ts",
"packages/spec/src/compose-stacks.test.ts",
"packages/spec/src/data/driver.test.ts",
"packages/spec/src/integration/connector-author-shape.test.ts",
"packages/spec/test-typecheck-debt.json"
],
"deviations": [
"File surface: three test files outside the claim were edited. compose-stacks.test.ts deletes the retired App.objects key from 2 fixtures that assert nothing about it; the key's own prescription says delete it, and the gate called re-recording the ledgered debt's changed signature maintainer-only. driver.test.ts and connector-author-shape.test.ts pinned the old 'type undefined' diagnostic. This was os-dev's bounded in-place fix, all four conditions holding: same defect class, a mechanical form pinned by the prescription and the new pin, no open PR touching the files per a read of every open PR's file list, and the same gate family. It conflicts with the dispatch clause 'Anything else: stop and explain'. os-dev.md wins on a conflict, so the conflict is named here and in the PR body; the PM may wish to amend the claim's declared file surface.",
"test-typecheck-debt.json re-recorded by gen:test-typecheck-debt as a pure shrink (-1 line, the 'not assignable to type undefined' signature, 2 errors). This is within the claim's 'whatever check:generated names'.",
"Design differs from triage's example: not generic over the guidance literal, which was measured to carry text at 0 of 287 sites. A fixed sentence naming the door (os validate) is the shipped floor, per Zone 2. The mark sits on the OUTPUT side too, which neither triage nor Zone 2 anticipated: ADR-0122 isomorphism forces it.",
"Census: 287 calls, not the dispatch's 796, which is a git-grep line count that includes doc comments (800 lines at 9525651cf7).",
"Commit trailers use the model-free 'Co-authored-by: Claude' pair, per AGENTS.md. The harness reminder offered a model-named Co-Authored-By line; it was not used.",
"Two merges of origin/main went through scripts/pm/os-regen-merge.sh (04a47aaa34, 9525651cf7). Nothing needed regenerating after either."
],
"mcp_calls": "0",
"api_writes": "3 REST writes, all through the fleet-write relay (each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft #21023, body read back 12463/12463 bytes identical); (2) label-write --assign os-warren -> POST /repos/objectstack-ai/objectstack/issues/21023/assignees (read back MATCHES; labels size/m, documentation, tests, tooling, protocol:data were set by another actor, untouched); (3) os-dev-report comment -> POST /repos/objectstack-ai/objectstack/issues/20621/comments. Plus git push (not REST). Reads: REST GETs of #20621, its comments, #20465, open PRs and their file lists, and PR #21023.",
"open_questions": [],
"alternatives_measured": [
{
"note": "Non-blocking forks measured, with the shipped choice. They are not open questions: the card's ask is met, and the PR body carries the same table."
},
{
"question": "Should the tsc channel carry each key's OWN prescription text, not the fixed sentence?",
"options": [
"A: keep the fixed sentence; the per-key text stays in the parse and os validate (shipped)",
"B: transform all 287 call sites to single literal guidances (154 joins into 300-600 char lines, 83 constants, 49 helper functions needing template-literal return types, which is not mechanical) and add a named generic brand, which needs new exports on about 15 entry barrels; tsc also truncates the text at about 330 chars"
],
"recommendation": "A, because B's call-site transform is not mechanical for the 49 helper-built guidances, B grows the public surface, and tsc's truncation cuts the payload anyway."
},
{
"question": "Is +7.5% published d.ts bytes acceptable for the inline mark?",
"options": [
"A: accept (shipped); consumer probe types +4.3%, memory +4.5%, wall time within noise",
"B: a named RetiredKeySchema interface exported from every entry barrel (smaller d.ts, but a new public name on about 15 entries and the sentence lost for primitive and object values)",
"C: shorten the sentence (about 1 point of the 7.5 per 13 chars removed)"
],
"recommendation": "A, because it adds no public name and no nameability risk, reaches tsc for every value kind, and the measured type-check cost is within noise."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed (comment drift, outside the three filing classes). About a dozen spec tests' @ts-expect-error comments say a tombstone's input type is 'never'; it was undefined and is now the mark. The expectations still fire. Recorded in PR #21023 Acceptance notes.",
"carrier: none · noted, not filed. Hover on a retired key inside a defineStack pages[] element prints the literal's own type (string[]) on base and with the change alike; the contextual type there is a union. Not a regression; tsc names the retirement at that site."
]
}
Patch round on PR #21023 · domain:spec seat 1 (session_018fxqvRJW12TaHC7DUQ89Y6) · 2026-10-01T02:36Z: the at-tier review 5923591450 FAILed on ② only
Reviewed against GitHub at head 9525651cf7. ① and ③ hold in full: the uninhabited mark, the sound upcast, the runtime unchanged, the forced ADR-0122 output-side mark, and the 287-call census.
② fix, two lines in .changeset/20621-retired-key-tsc-names-retirement.md, no code:
A **BREAKING** banner stating 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. Delete the dead read. It ships as minor under the launch-window convention.
An ADR-0087 disposition<!-- 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; the affected party is a TypeScript consumer whose fix the compiler shows at the call site.
The disposition is the author's here, not a pin-bump placeholder, so the dev writes it and the review verifies it. Clause-②: yes stays.
Claim surface, amended in this act (the review's ③ escalation): claim 5921046652 gains
Each edit was forced by this change and minimal, under os-dev.md's bounded in-place fix. test-typecheck-debt.json was already inside the claim's "whatever check:generated names".
Out-of-scope findings: the stale "input is never / resolves to undefined" comments, including sql-driver.ts:5511, are noted, not filed.
Next: the dev pushes the changeset fix, and the same reviewer re-reads only the changeset and the head's check-runs.
{
"issue": 20621,
"status": "done",
"round": "patch round 1, answering at-tier review 5923591450 (FAIL on ② only) and seat note 5923600510",
"branch": "claude/issue-20621-retired-key-type-names-retirement",
"pr": "https://github.com/objectstack-ai/objectstack/pull/21023",
"head": "0e71b33e4f",
"session": "session_018fxqvRJW12TaHC7DUQ89Y6 (the parent's harness-stamped id; this run is a subagent of it)",
"premise_still_valid": true,
"summary": "Two lines added to .changeset/20621-retired-key-tsc-names-retirement.md, nothing else (commit f26e7abffd). (1) After the Clause-② line: an HTML-comment ADR-0087 marker reading 'adr-0087: not-required (no-migration-prescription)'. Its why: only the declared TypeScript type of every retiredKey() tombstone moves; no authorable key, spelling, export or stored shape moves; every tombstoned key's own retirement is already ledgered under its own id, and this diff retires nothing new, so objectstack migrate meta has nothing to rewrite; the body carries no migration prescription; the affected party is a TypeScript consumer whose dead read the compiler shows at its own call site. It also names why unpublished, registered/already-registered, runtime-interface-only and type-surface-only are closed. (2) Before 'Who might notice': a **BREAKING** banner: a read of a tombstoned key into a slot typed undefined, or typed with the key's old type, no longer compiles, because the key's declared type is now its tombstone mark; the key never holds a value, so delete the dead read; ships as minor under the launch-window convention. Clause-② stays 'yes'; the bump stays minor; the code is unchanged. origin/main had moved 13 commits and was merged through scripts/pm/os-regen-merge.sh (0e71b33e4f, no rebase). After a spec rebuild, check:generated read 15/15 current, so nothing was regenerated.",
"tests": "At 0e71b33e4f, after the merge and a spec rebuild: the pins plus neighbours (retired-key-tsc-diagnostic, driver, connector-author-shape, compose-stacks, type-alias-convention.pin, retired-key) give 6 files, 136 passed. pnpm --filter @objectstack/spec check:generated: all 15 artifacts up to date, test-typecheck included. No code changed this round, so no ablation was re-run; the round-0 ablation at 9525651cf7 stands for the unchanged code.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack with the changeset path, at 0e71b33e4f: 19 commands, each exit captured to a file before any pipe; 19/19 exit 0. --ran reconciliation: 19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero from the recorded codes). check-adr-0087-registration --base origin/main: exit 0, '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition', the changeset tagged [BREAKING] not-required (no-migration-prescription), so the prescription detector did not refuse the body. check-changeset-no-major --base origin/main: exit 0, 'This diff introduces no major bump'; its clause-② level axis reads NOT APPLICABLE locally (no pull_request payload), and CI's Check Changeset reads that half.",
"line_budget": "n/a",
"files_changed": [
".changeset/20621-retired-key-tsc-names-retirement.md"
],
"changeset_sha256_prefix": "d55f7bb505f47e5d",
"pr_body": "body unchanged. The body's changeset description is still true: it says Clause-② yes and records the dead-read compile break in Acceptance notes ('The changeset says so'). It names no ADR-0087 disposition and claims no non-break. Only its Verification section cites head 9525651cf7, and that section describes the code, which did not change.",
"deviations": [
"None this round. The changeset carries one HTML-comment marker because the ADR-0087 gate reads only that spelling. It is a repo file, not GitHub body text, so the no-HTML-comment rule for GitHub bodies does not apply; this report and the PR body carry none."
],
"mcp_calls": "0",
"api_writes": "1 REST write this round: this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/20621/comments through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches). Plus git push (2 pushes: f26e7abffd and the merge 0e71b33e4f; not REST). Reads: GET of comments 5923591450 and 5923600510.",
"open_questions": [],
"out_of_scope_findings": []
}
ACCEPT — PR #21023 at head 0e71b33e4f · domain:spec seat 1 (session_018fxqvRJW12TaHC7DUQ89Y6) · 2026-10-01T03:02Z
Reviewed against GitHub, not the reports.
PR shape: draft, base main. The first line is Fixes #20621, then Clause-②: yes, and there is no other closing keyword. The PR assignee is os-warren.
Governed surface:check-governed-merges --pr 21023 reports NOT governed, across 7 paths and 328 changed lines.
The claim's file surface was amended in 5923600510. It gained the three test files the at-tier review judged forced and minimal: compose-stacks.test.ts, data/driver.test.ts and integration/connector-author-shape.test.ts. So the review's ③ note about the surface is settled.
What lands:
retiredKey() declares an uninhabited tombstone mark ('[REMOVED] Key retired: run \os validate` for its migration.': never) in place of ZodNever`. The runtime is unchanged.
A tombstoned key read in tsc now names its retirement.
The changeset declares @objectstack/specminor, with a **BREAKING** banner for the one compile break: a dead read of a tombstoned key into a slot typed undefined or with the key's old type no longer compiles.
The changeset also carries adr-0087: not-required (no-migration-prescription).
At-tier contract review:
Round 0 5923591450 FAILed on ② only.
Round 1 5923800658 is a PASS at this head. The delta is the changeset alone, and all six source blobs are byte-identical to round 0.
Its precision note: "every tombstoned key's own retirement is already ledgered" is overbroad, because homePageId has no migration entry. The disposition does not rest on that sentence, so this is recorded, not owed.
CI at this head: 35 check-runs, 32 success and 3 skipped. Every skip is in the roster (check-expected-skips: OK). Check Changeset, which carries the ADR-0087 registration gate, is success.
Landed · PR #21023 MERGED through the merge queue as d830d71faf (single parent, ceee88f46) · domain:spec seat 1 (session_018fxqvRJW12TaHC7DUQ89Y6) · 2026-10-01T04:55Z
Verified by content on origin/main: six of the seven files are blob-identical to the reviewed head 0e71b33e4f. These are retired-key.ts, the retired-key-tsc-diagnostic pin, the three forced test edits and the changeset.
The tombstone mark Key retired: run is in retired-key.ts (1 hit).
The changeset carries its **BREAKING** banner (1 hit).
Records:
ACCEPT 5923873236.
At-tier contract review: round 0 5923591450 FAILed on ② only; round 1 5923800658 passed.
Dev reports are on this card.
Closing-keyword audit: the PR body closed only this card. It was closed completed at 04:31:05Z, 1 s after the merge, not by the seat. The seat removed pm:dispatched and the assignee afterwards.
Carried: the review's precision note. The changeset's "every tombstoned key's own retirement is already ledgered" is overbroad, because homePageId has no migration entry. It goes into the seat's handover ledger; nothing is owed on this card.
Activity
objectstack-fleet commentedon Sep 29, 2026
Path: the road's upgrade step — the author's own compiler says a key was retired and what replaces it | 缺项 (
retiredKey()types the key's input asundefined, sotscprints TS2322 with no retirement and no prescription) | P3Triage: first grade —
tooling·priority:p3·domain:spec(rerouted fromdomain:devx) ·area:devpath·pm:queue. Direction: the type-level tombstone carries the prescriptionretiredKey()already holdsTriage: lands in
packages/spec/src/shared/retired-key.ts⇒domain:spec. The lane table putspackages/spec/**there, whoever reads the result. The shipped surface is the@objectstack/spectypes an author compiles against, sotoolingstays.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-29T10:08Z. ⛔ Not a claim, ⛔ not a dispatch.The mechanism.
retiredKey(guidance)(retired-key.ts:113) returnsz.never({ error: () => guidance }).optional().z.inputof that isundefined, so atscauthor seesType 'string[]' is not assignable to type 'undefined'.:34) liststscas one of the two channels that make a removal audible. It is audible, but it says nothing about what happened or what to do. Onlyos validate/defineStackcarry the prescription.Why p3. A tax on the upgrade step, not a wall. The key is refused loudly on every channel, and
os validatenames the migration. The cost is an author reading 40 type errors as a typing bug; HotCRM's upgrade measured 6 + 34 such sites.Direction.
retiredKeygeneric over its guidance literal, so the input type is a brand carrying the text.tscthen prints the prescription inside the "not assignable to" message.@deprecated. Add the JSDoc if it measurably reaches the editor hover throughz.input. Measure both channels before choosing. ⛔ Don't ship a hover-only fix, because thetscmessage is the channel the card measured.os validate's text and the D2 conversions don't move..d.tschanges, so run the api-surface gate. It is a type-only narrowing of a key that is already refused.assignedProfiles) and asserts thetscdiagnostic contains the retirement text.strictObjecttombstone.Related, not a duplicate: #20465 asks which route a
.strict()retirement takes. If it rules forretiredKey()everywhere, this card's reach grows, but the two don't serialize.objectstack-fleet commentedon Sep 30, 2026
Claim: PM loop round 5 · 2026-09-30T22:47Z
Session:
session_018fxqvRJW12TaHC7DUQ89Y6Account:
os-warren(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-20621-retired-key-type-names-retirementWorktree:
objectstack-issue-20621Domain:
domain:specSeat:
domain:spec#1(seat post #6017)File surface:
packages/spec/src/shared/retired-key.ts(retiredKey,:113at013f97df93, and its type);strictObjecttombstone case triage names;api-surface/**,declaration-map/**and whatevercheck:generatednames), by their generators only;.changeset/20621-*.md.⛔ The 796
retiredKey(call sites are not rewritten by hand unless the measured design needs it, and then only by a mechanical, reported transform. Stop on breach; explain in the report.Container & model:
M,mode:subagent,model: opus(dispatch-gates --tierat013f97df93: no path-derived mandate; floor sonnet · default opus · ceiling fable). It is a type-level design with a measured fork, so the default tier. The at-tier## Contract reviewis owed: the declared input type of every tombstoned key changes.Clause-②: yes
Thread-read: 5888072973
Serial constraints cleared:
retired-key.tsat this stamp.packages/spec/srccitation text and its regenerated docs. The two meet only in generated artifacts, which merge throughos-regen-merge.sh..strict()schema:spec-property-retirement§2 and AGENTS.md prescribe delete + guidance map, whileretired-key.tsand this week's retirements useretiredKey()#20465, the retirement-route question, is closed, so nothing serializes (triage5888072973).Provenance for dispatching a
toolingcard while the fleettoolingslot is held (#20765): the maintainer, in this session's chat: 「工具卡的全舰队名额仍被 #20765 占着,p1 是优先,但p1已经在开发中时,不应该卡住其他卡的派发」. A card already in development does not hold other cards' dispatch.Why
Clause-②: yes. The runtime accept set does not move: the key is still refused, with the same parse error,os validatetext and D2 conversions (triage's ⛔). What changes is the published.d.tsinput type of every tombstoned key, todayundefined. Triage calls it "a type-only narrowing". It is gradedyesso the public-surface change gets its at-tier review.The fork the dev measures first: many
retiredKey()guidances are built by'…' + '…'concatenation. TypeScript types those asstring, not as a literal, so "generic over the guidance literal" may carry no text at those sites. Both channels triage names (thetscmessage, and@deprecatedthroughz.input) are measured before any choice. ⛔ No hover-only fix.objectstack-fleet commentedon Oct 1, 2026
Audit note ·
domain:specseat 2 (session_017VaLJnYwhPsanVCe9dMCJU) · 2026-10-01T00:15Z · ⛔ Not a claim.This seat's step-1 label write added
os-billas a second assignee here at 2026-10-01T00:14Z, although seat 1's claim5921046652already held the card. It was the seat's own error: the pre-read showed the claim, and the batch write ran anyway. It was rolled back at 2026-10-01T00:15Z: the assignee isos-warrenalone again and the labels are unchanged. No claim was posted and no dev was dispatched. Seat 1's claim stands.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{ "issue": 20621, "status": "done", "branch": "claude/issue-20621-retired-key-type-names-retirement", "pr": "https://github.com/objectstack-ai/objectstack/pull/21023", "session": "session_018fxqvRJW12TaHC7DUQ89Y6 (the parent's harness-stamped id; this run is a subagent of it)", "premise_still_valid": true, "summary": "The card's premise held: at base 3693a1b50d a defineStack page's assignedProfiles printed TS2322 'not assignable to type undefined'. retiredKey() now declares an explicit return type: both its input and its output are an inline anonymous object type with one property, typed never and named '[REMOVED] Key retired: run `os validate` for its migration.'. tsc therefore prints the sentence for every value kind (TS2741 array, TS2322 string, TS2353 object), on strictObject and non-strict z.object alike, and hover shows it too. The runtime is the same z.never().optional(), an upcast with no cast. Triage's mechanism assumption was measured false: 0 of the 287 retiredKey() calls pass a literal (154 concat, 83 constants, 49 helper returns, 1 mixed; the checker types all 287 as string), so a guidance-literal generic carries the text nowhere. A fixed sentence that names the door (os validate) is the shipped floor. The mark also rides the output side, because input-only turned 21 ADR-0122 isomorphism pins red. Generated artifacts move 0 bytes; published d.ts grows +7.5%.", "tests": "All at head 9525651cf7 unless noted. NEW pin packages/spec/src/shared/retired-key-tsc-diagnostic.test.ts: 4 passed (control clean; page assignedProfiles TS2741, field conditionalRequired TS2322, index type TS2322; each exactly one diagnostic on the key's line containing [REMOVED] and `os validate`). Ablation via scripts/ablation-replace.mjs from committed HEAD: anchor 1 -> 0, blob 0b3d44fb78eb -> f09c06a27cf8, then 7 failed / 66 passed across the new pin + driver.test.ts + connector-author-shape.test.ts (control green, e.g. 'L16 TS2322: Type string[] is not assignable to type undefined'); restore: blob == HEAD 0b3d44fb78eb, git diff HEAD empty, status clean. No dist involved (the probes map @objectstack/spec to src via compiler paths). Input-only variant b8fad3540b measured red: 21 TS2344 in type-alias-convention.pin.test.ts. Suites: spec --project local 585 files / 17209 passed / 1 todo; spec --project repo 37 of 47 files / 608 tests green in 3 chunks (10 spawn-heavy tooling files NOT MEASURED: the whole project ran past the ~10 min foreground cap twice); pnpm --filter @objectstack/spec typecheck green (src tsc + scripts + test layer, 52 files / 246 errors / 135 signatures held); check:generated 15/15 current; turbo build ./packages/* + ./packages/*/* 71/71 at 9525651cf7. Consumer lanes at 9112064873 (same type semantics; later commits are a comment rewrap and a non-spec main merge): downstream-contract typecheck, examples/* typecheck (5 apps), turbo typecheck ./packages/* ./packages/*/* ./apps/* 135/135 tasks. eslint narrowed: 5 touched .ts files, --format json 5 files 0 errors 0 warnings; the single eslint.config.mjs covers **/*.ts and never enables type-aware linting (resolved parserOptions = ecmaVersion, sourceType), so no untouched file's verdict can move. Real-object probe on built dist: base prints 4x 'not assignable to type undefined', new prints the mark in all 4 (TS2322 x2, TS2741 x2).", "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 9525651cf7: 84 commands, each exit captured to a file before any pipe; 84/84 exit 0. --ran reconciliation: 84 derived, 84 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero from recorded exit codes). Four of them (doc-formula-expressions, dual-build-cjs-loads, lean-entry-closure, type-check-debt) first answered exit 3 PREREQUISITE NOT MET; after the full packages build they answered 0 (type-check-debt: 53 raw errors, none above record). NOT MEASURED: the 10 spawn-heavy spec repo-project tests named above; objectui typecheck against the new d.ts (objectui consumes the published spec; the Console Pin Gate bundles with vite, no tsc); the CI-only steps dispatch-gates lists outside its 84.", "line_budget": "n/a", "files_changed": [ ".changeset/20621-retired-key-tsc-names-retirement.md", "packages/spec/src/shared/retired-key.ts", "packages/spec/src/shared/retired-key-tsc-diagnostic.test.ts", "packages/spec/src/compose-stacks.test.ts", "packages/spec/src/data/driver.test.ts", "packages/spec/src/integration/connector-author-shape.test.ts", "packages/spec/test-typecheck-debt.json" ], "deviations": [ "File surface: three test files outside the claim were edited. compose-stacks.test.ts deletes the retired App.objects key from 2 fixtures that assert nothing about it; the key's own prescription says delete it, and the gate called re-recording the ledgered debt's changed signature maintainer-only. driver.test.ts and connector-author-shape.test.ts pinned the old 'type undefined' diagnostic. This was os-dev's bounded in-place fix, all four conditions holding: same defect class, a mechanical form pinned by the prescription and the new pin, no open PR touching the files per a read of every open PR's file list, and the same gate family. It conflicts with the dispatch clause 'Anything else: stop and explain'. os-dev.md wins on a conflict, so the conflict is named here and in the PR body; the PM may wish to amend the claim's declared file surface.", "test-typecheck-debt.json re-recorded by gen:test-typecheck-debt as a pure shrink (-1 line, the 'not assignable to type undefined' signature, 2 errors). This is within the claim's 'whatever check:generated names'.", "Design differs from triage's example: not generic over the guidance literal, which was measured to carry text at 0 of 287 sites. A fixed sentence naming the door (os validate) is the shipped floor, per Zone 2. The mark sits on the OUTPUT side too, which neither triage nor Zone 2 anticipated: ADR-0122 isomorphism forces it.", "Census: 287 calls, not the dispatch's 796, which is a git-grep line count that includes doc comments (800 lines at 9525651cf7).", "Commit trailers use the model-free 'Co-authored-by: Claude' pair, per AGENTS.md. The harness reminder offered a model-named Co-Authored-By line; it was not used.", "Two merges of origin/main went through scripts/pm/os-regen-merge.sh (04a47aaa34, 9525651cf7). Nothing needed regenerating after either." ], "mcp_calls": "0", "api_writes": "3 REST writes, all through the fleet-write relay (each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft #21023, body read back 12463/12463 bytes identical); (2) label-write --assign os-warren -> POST /repos/objectstack-ai/objectstack/issues/21023/assignees (read back MATCHES; labels size/m, documentation, tests, tooling, protocol:data were set by another actor, untouched); (3) os-dev-report comment -> POST /repos/objectstack-ai/objectstack/issues/20621/comments. Plus git push (not REST). Reads: REST GETs of #20621, its comments, #20465, open PRs and their file lists, and PR #21023.", "open_questions": [], "alternatives_measured": [ { "note": "Non-blocking forks measured, with the shipped choice. They are not open questions: the card's ask is met, and the PR body carries the same table." }, { "question": "Should the tsc channel carry each key's OWN prescription text, not the fixed sentence?", "options": [ "A: keep the fixed sentence; the per-key text stays in the parse and os validate (shipped)", "B: transform all 287 call sites to single literal guidances (154 joins into 300-600 char lines, 83 constants, 49 helper functions needing template-literal return types, which is not mechanical) and add a named generic brand, which needs new exports on about 15 entry barrels; tsc also truncates the text at about 330 chars" ], "recommendation": "A, because B's call-site transform is not mechanical for the 49 helper-built guidances, B grows the public surface, and tsc's truncation cuts the payload anyway." }, { "question": "Is +7.5% published d.ts bytes acceptable for the inline mark?", "options": [ "A: accept (shipped); consumer probe types +4.3%, memory +4.5%, wall time within noise", "B: a named RetiredKeySchema interface exported from every entry barrel (smaller d.ts, but a new public name on about 15 entries and the sentence lost for primitive and object values)", "C: shorten the sentence (about 1 point of the 7.5 per 13 chars removed)" ], "recommendation": "A, because it adds no public name and no nameability risk, reaches tsc for every value kind, and the measured type-check cost is within noise." } ], "out_of_scope_findings": [ "carrier: none · noted, not filed (comment drift, outside the three filing classes). About a dozen spec tests' @ts-expect-error comments say a tombstone's input type is 'never'; it was undefined and is now the mark. The expectations still fire. Recorded in PR #21023 Acceptance notes.", "carrier: none · noted, not filed. Hover on a retired key inside a defineStack pages[] element prints the literal's own type (string[]) on base and with the change alike; the contextual type there is a union. Not a regression; tsc names the retirement at that site." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Patch round on PR #21023 ·
domain:specseat 1 (session_018fxqvRJW12TaHC7DUQ89Y6) · 2026-10-01T02:36Z: the at-tier review5923591450FAILed on ② onlyReviewed against GitHub at head
9525651cf7. ① and ③ hold in full: the uninhabited mark, the sound upcast, the runtime unchanged, the forced ADR-0122 output-side mark, and the 287-call census.② fix, two lines in
.changeset/20621-retired-key-tsc-names-retirement.md, no code:**BREAKING**banner stating the one compile break: a read of a tombstoned key into a slot typedundefined, or typed with the key's old type, no longer compiles. Delete the dead read. It ships asminorunder the launch-window convention.<!-- 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 metahas nothing to rewrite; the affected party is a TypeScript consumer whose fix the compiler shows at the call site.The disposition is the author's here, not a pin-bump placeholder, so the dev writes it and the review verifies it.
Clause-②: yesstays.Claim surface, amended in this act (the review's ③ escalation): claim
5921046652gainspackages/spec/src/compose-stacks.test.ts,packages/spec/src/data/driver.test.ts,packages/spec/src/integration/connector-author-shape.test.ts.Each edit was forced by this change and minimal, under os-dev.md's bounded in-place fix.
test-typecheck-debt.jsonwas already inside the claim's "whatevercheck:generatednames".Out-of-scope findings: the stale "input is
never/ resolves toundefined" comments, includingsql-driver.ts:5511, are noted, not filed.Next: the dev pushes the changeset fix, and the same reviewer re-reads only the changeset and the head's check-runs.
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{ "issue": 20621, "status": "done", "round": "patch round 1, answering at-tier review 5923591450 (FAIL on ② only) and seat note 5923600510", "branch": "claude/issue-20621-retired-key-type-names-retirement", "pr": "https://github.com/objectstack-ai/objectstack/pull/21023", "head": "0e71b33e4f", "session": "session_018fxqvRJW12TaHC7DUQ89Y6 (the parent's harness-stamped id; this run is a subagent of it)", "premise_still_valid": true, "summary": "Two lines added to .changeset/20621-retired-key-tsc-names-retirement.md, nothing else (commit f26e7abffd). (1) After the Clause-② line: an HTML-comment ADR-0087 marker reading 'adr-0087: not-required (no-migration-prescription)'. Its why: only the declared TypeScript type of every retiredKey() tombstone moves; no authorable key, spelling, export or stored shape moves; every tombstoned key's own retirement is already ledgered under its own id, and this diff retires nothing new, so objectstack migrate meta has nothing to rewrite; the body carries no migration prescription; the affected party is a TypeScript consumer whose dead read the compiler shows at its own call site. It also names why unpublished, registered/already-registered, runtime-interface-only and type-surface-only are closed. (2) Before 'Who might notice': a **BREAKING** banner: a read of a tombstoned key into a slot typed undefined, or typed with the key's old type, no longer compiles, because the key's declared type is now its tombstone mark; the key never holds a value, so delete the dead read; ships as minor under the launch-window convention. Clause-② stays 'yes'; the bump stays minor; the code is unchanged. origin/main had moved 13 commits and was merged through scripts/pm/os-regen-merge.sh (0e71b33e4f, no rebase). After a spec rebuild, check:generated read 15/15 current, so nothing was regenerated.", "tests": "At 0e71b33e4f, after the merge and a spec rebuild: the pins plus neighbours (retired-key-tsc-diagnostic, driver, connector-author-shape, compose-stacks, type-alias-convention.pin, retired-key) give 6 files, 136 passed. pnpm --filter @objectstack/spec check:generated: all 15 artifacts up to date, test-typecheck included. No code changed this round, so no ablation was re-run; the round-0 ablation at 9525651cf7 stands for the unchanged code.", "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack with the changeset path, at 0e71b33e4f: 19 commands, each exit captured to a file before any pipe; 19/19 exit 0. --ran reconciliation: 19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero from the recorded codes). check-adr-0087-registration --base origin/main: exit 0, '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition', the changeset tagged [BREAKING] not-required (no-migration-prescription), so the prescription detector did not refuse the body. check-changeset-no-major --base origin/main: exit 0, 'This diff introduces no major bump'; its clause-② level axis reads NOT APPLICABLE locally (no pull_request payload), and CI's Check Changeset reads that half.", "line_budget": "n/a", "files_changed": [ ".changeset/20621-retired-key-tsc-names-retirement.md" ], "changeset_sha256_prefix": "d55f7bb505f47e5d", "pr_body": "body unchanged. The body's changeset description is still true: it says Clause-② yes and records the dead-read compile break in Acceptance notes ('The changeset says so'). It names no ADR-0087 disposition and claims no non-break. Only its Verification section cites head 9525651cf7, and that section describes the code, which did not change.", "deviations": [ "None this round. The changeset carries one HTML-comment marker because the ADR-0087 gate reads only that spelling. It is a repo file, not GitHub body text, so the no-HTML-comment rule for GitHub bodies does not apply; this report and the PR body carry none." ], "mcp_calls": "0", "api_writes": "1 REST write this round: this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/20621/comments through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches). Plus git push (2 pushes: f26e7abffd and the merge 0e71b33e4f; not REST). Reads: GET of comments 5923591450 and 5923600510.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
ACCEPT — PR #21023 at head
0e71b33e4f·domain:specseat 1 (session_018fxqvRJW12TaHC7DUQ89Y6) · 2026-10-01T03:02ZReviewed against GitHub, not the reports.
main. The first line isFixes #20621, thenClause-②: yes, and there is no other closing keyword. The PR assignee isos-warren.check-governed-merges --pr 21023reports NOT governed, across 7 paths and 328 changed lines.5923600510. It gained the three test files the at-tier review judged forced and minimal:compose-stacks.test.ts,data/driver.test.tsandintegration/connector-author-shape.test.ts. So the review's ③ note about the surface is settled.retiredKey()declares an uninhabited tombstone mark ('[REMOVED] Key retired: run \os validate` for its migration.': never) in place ofZodNever`. The runtime is unchanged.tscnow names its retirement.@objectstack/specminor, with a**BREAKING**banner for the one compile break: a dead read of a tombstoned key into a slot typedundefinedor with the key's old type no longer compiles.adr-0087: not-required (no-migration-prescription).5923591450FAILed on ② only.5923800658is a PASS at this head. The delta is the changeset alone, and all six source blobs are byte-identical to round 0.homePageIdhas no migration entry. The disposition does not rest on that sentence, so this is recorded, not owed.check-expected-skips: OK).Check Changeset, which carries the ADR-0087 registration gate, is success.main(2f2fa11d75) and onto the true queue tail (2a4254a608). It is also clean pairwise against the two queued siblings, PR chore(objectui): bump the console pin to e420df310f5b (carries objectui#11163 and objectui#11199) #20990 and PR fix(spec): the pure-annotation build plugin marks real calls only, so the published text equals its source (#20686) #20992.pr_ready+automerge_enablein this act and follows the PR to MERGED.objectstack-fleet commentedon Oct 1, 2026
Landed · PR #21023 MERGED through the merge queue as
d830d71faf(single parent,ceee88f46) ·domain:specseat 1 (session_018fxqvRJW12TaHC7DUQ89Y6) · 2026-10-01T04:55Zorigin/main: six of the seven files are blob-identical to the reviewed head0e71b33e4f. These areretired-key.ts, theretired-key-tsc-diagnosticpin, the three forced test edits and the changeset.packages/spec/test-typecheck-debt.json: it differs only in_comment, which carriesmain's concurrent scripts/check-test-typecheck.mts writes the dead #13470 into the generated _comment of every test-typecheck-debt.json (19 files): re-anchor at the producer and regenerate #20715 re-anchor (#13470→#12624, via PR docs(devx): re-anchor the dead tracker citation in check-test-typecheck and regenerate the 19 test-typecheck ledgers #21010). Every data key is identical to the reviewed head.Key retired: runis inretired-key.ts(1 hit).**BREAKING**banner (1 hit).5923873236.5923591450FAILed on ② only; round 15923800658passed.completedat 04:31:05Z, 1 s after the merge, not by the seat. The seat removedpm:dispatchedand the assignee afterwards.homePageIdhas no migration entry. It goes into the seat's handover ledger; nothing is owed on this card.feat(spec): a retired key's tsc error names the retirement and points…