|
| 1 | +# ADR-0145: A `?: never` tombstone on the ui authoring face retires on the spec's own window — one major after the retirement it names, never longer |
| 2 | + |
| 3 | +**Status**: Proposed (2026-10-28). `docs/adr/**` is Tier H (Prime Directive #14): this record becomes **Accepted** by |
| 4 | +the maintainer's merge. **This record declares; it implements nothing** — no tombstone is removed, no code, no schema, |
| 5 | +no changeset. The first application batch is its own card, cut after the merge (see [Applying the rule](#applying-the-rule)). |
| 6 | +**Decided by**: the maintainer's letter **A** of 2026-10-25T01:46Z on |
| 7 | +[#25912](https://github.com/objectstack-ai/objectstack/issues/25912) (filed as objectui#13467), transcribed as ruling |
| 8 | +[`6407810703`](https://github.com/objectstack-ai/objectstack/issues/25912#issuecomment-6407810703) over the seat's |
| 9 | +options [`6407810621`](https://github.com/objectstack-ai/objectstack/issues/25912#issuecomment-6407810621). Executed |
| 10 | +here as written and ⛔ not re-opened. Not taken: **B** (a tombstone leaves the day the spec retires its key — the remedy |
| 11 | +would die before the authors on the previous major stop needing it) and **C** (no rule — the face only grows). |
| 12 | +**Builds on**: [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) (D2's one-major conversion window and its |
| 13 | +2026-10-26 addendum, "retirement kits age out after one major" — the window this record reads, ⛔ never restates or |
| 14 | +changes), [ADR-0049](./0049-no-unenforced-security-properties.md) (the retirement-tombstone convention the ui mirrors |
| 15 | +spell through `retirementTombstone`), AGENTS.md Prime Directive #12 (a deletion that turns a refusal into a silent |
| 16 | +accept is the defect it names) and Post-Task Checklist step 3 (the changeset). |
| 17 | +**Governs**: every `?: never` member on the ui TypeScript authoring face — `ui/packages/types` and any `.d.ts` another |
| 18 | +`ui/packages/*` package publishes — and the zod twin that mirrors it. ⛔ Not a live `@deprecated` member (an alias the |
| 19 | +runtime still accepts): it is not a tombstone, and its retirement is its own card's. |
| 20 | +**Registry**: the root `docs/adr/` (Prime Directive #13: one registry per repository). `ui/docs/adr/` is the imported |
| 21 | +ObjectUI sequence, kept in place, and gains no record here. |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## Context |
| 26 | + |
| 27 | +The ui authoring face refuses a retired key **by name**. `BaseSchema` declares no index signature (ui/AGENTS.md #0.1), |
| 28 | +so a fresh node literal refuses any key no declaration names; a retired key is kept declared as `key?: never` with a |
| 29 | +`/** @deprecated RETIRED (…) … */` JSDoc whose text is the remedy, and the zod mirror declares the same key through |
| 30 | +`retirementTombstone` (or a sibling helper in `zod/tombstone.zod.ts`), so a document that arrives as data is refused at |
| 31 | +the key's own path with the same remedy. That is #0.1 working as designed. |
| 32 | + |
| 33 | +What was missing is the exit. Nothing said when a tombstone leaves, so every retirement became a permanent member of |
| 34 | +the published `.d.ts`: an AI author reading the face sees the refused keys beside the live ones, and the longer that |
| 35 | +list, the more the face teaches what not to write instead of what to write. |
| 36 | + |
| 37 | +The spec already answers the same question for its own face. ADR-0087 D2 applies each conversion for one major, and |
| 38 | +its 2026-10-26 addendum carries the retirement kit — the `retiredKey()` tombstone with its conversion — through the |
| 39 | +protocol major after the one it retired in, removing it when the next major is cut; after that the spec's strict |
| 40 | +container refuses the key as an unknown one and the prescription lives in the protocol upgrade guide, the CHANGELOG |
| 41 | +and git. The same addendum keeps a tombstone whose container **strips** unknown keys, because deleting it would turn a |
| 42 | +refusal into a silent strip. The ui tombstone mirrors the spec's; this record reads that window on the ui face. |
| 43 | + |
| 44 | +## Decision |
| 45 | + |
| 46 | +### D1 — The rule |
| 47 | + |
| 48 | +A `?: never` tombstone leaves the ui authoring face in the first release that follows the spec major in which the |
| 49 | +retirement it names has its ADR-0087 conversion expire: **one major after the retirement**, the window the spec itself |
| 50 | +runs, ⛔ never a longer one. Its remedy JSDoc ships for exactly that window. |
| 51 | + |
| 52 | +Spelled on ADR-0087's clock, with **R** the protocol major the retirement belongs to (D2 says how R is read): |
| 53 | + |
| 54 | +- the tombstone is carried through major **R + 1**, and is **due** from the cut of major **R + 2** — the same cut at |
| 55 | + which the spec's kit for a key retired in R leaves (ADR-0087, 2026-10-26 addendum); |
| 56 | +- "major" is the **protocol** major (`PROTOCOL_VERSION`, the `CURRENT_MAJOR` the spec's deletion gate reads), ⛔ never |
| 57 | + the npm major: ADR-0087's pre-launch section keeps the two apart, and the `@object-ui/*` and `@objectstack/*` |
| 58 | + packages release as one fixed group, so one clock serves both faces; |
| 59 | +- the window binds in both directions: a tombstone that names a spec retirement ⛔ never leaves while the spec still |
| 60 | + carries that key's kit (the ui author would lose the remedy the spec still prints), and ⛔ never outlives it. |
| 61 | + |
| 62 | +### D2 — Reading R, including a retirement that predates ADR-0087 |
| 63 | + |
| 64 | +- **The tombstone names a spec retirement** (its JSDoc or zod guidance names the `@objectstack/spec` key or release it |
| 65 | + mirrors): R is the spec major that retired the key — the `RETIRED_KEYS_BY_MAJOR` row where one exists, otherwise the |
| 66 | + spec release clock ADR-0087's deletion gate uses (a key already `[RETIRED]` on the authorable surface at |
| 67 | + `@objectstack/spec@<M>.0.0` retired in a major below M). |
| 68 | +- **The spec retirement predates ADR-0087**, so no conversion was ever registered for it — the pre-launch one-step |
| 69 | + breaks of majors 12–15 that ADR-0087's "Ratified: the pre-launch launch-window exemption" section records, or a |
| 70 | + retirement older than D2 itself: it is counted **as if its conversion had run the one-major window from the major it |
| 71 | + retired in**. R is the major of the first spec release that refused the key. No such tombstone is granted a fresh |
| 72 | + window by this record: with the protocol at major 18, any whose R is 16 or below is already due, and the first |
| 73 | + application batch takes it. |
| 74 | +- **The tombstone names a ui-only retirement** (an objectui or ui card, no spec key behind it): the same window runs |
| 75 | + from the ui's own retirement. R is the major of the first `@object-ui/types` release tag (or the first release of the |
| 76 | + package that publishes the `.d.ts`) whose tree carries the tombstone. |
| 77 | +- **Where R cannot be read** (no tag, no row, a history the reader cannot reach): the tombstone is not due. The batch |
| 78 | + reports it as undated, ⛔ never guesses a date — a wrong date removes a remedy early, which is option B by accident. |
| 79 | + |
| 80 | +### D3 — A deletion must leave a refusal: the container carve-out |
| 81 | + |
| 82 | +The rule removes a **prescription**, never a **refusal**. ADR-0087's addendum keeps a spec tombstone whose container |
| 83 | +strips unknown keys; the same test applies on both ui faces, judged per key: |
| 84 | + |
| 85 | +- **TS face.** The `?: never` member leaves only where its container still refuses the key once it is gone — a type |
| 86 | + that declares no index signature, directly or through what it extends or intersects. Where the container is open, the |
| 87 | + member stays until the container is closed; closing it is its own card. |
| 88 | +- **Zod face.** The zod twin leaves in the same release only where every zod face that judges authored metadata still |
| 89 | + refuses the key once it is gone. `BaseSchemaCore` ends `.passthrough()`, so a member deleted from a container derived |
| 90 | + from it is **kept in silence** by the tolerant face (the case `agedOutSpecKeyRefusal` in `zod/tombstone.zod.ts` |
| 91 | + exists for: the ui keeps its own named refusal once the spec's tombstone ages out). Such a twin stays, as the ui's |
| 92 | + refusal, until its container refuses unknown keys — the posture the spec's strip-container tombstones keep. |
| 93 | +- **Derived faces move together.** Where the TS member is derived from the zod twin (`z.input` / `z.infer`), the two |
| 94 | + leave together or neither does. |
| 95 | + |
| 96 | +A tombstone held by D3 is listed by the batch with the container that holds it; it is not due, and it is not a |
| 97 | +defect of this rule. |
| 98 | + |
| 99 | +### D4 — The changeset that drops a tombstone |
| 100 | + |
| 101 | +- Level **`minor`**, ⛔ never `major` (`scripts/check-changeset-no-major.mjs` is the authority), on every package whose |
| 102 | + published face loses the member. |
| 103 | +- A **Breaking** note naming each removed key as `Type.key` and its remedy — the text of the JSDoc being deleted, |
| 104 | + carried over as the FROM → TO line — because from that release on the CHANGELOG is where an author who still writes |
| 105 | + the key finds what to write instead. |
| 106 | +- The declaration lines and the ADR-0087 disposition marker the changeset gates require (AGENTS.md Post-Task Checklist |
| 107 | + step 3); ⛔ their vocabulary is not restated here — the gates print it when they fail. |
| 108 | + |
| 109 | +### D5 — Not in this record |
| 110 | + |
| 111 | +- **No sweep.** This record removes nothing; the first application batch is its own card. |
| 112 | +- **The AGENTS.md pointer.** The ruling puts a one-line pointer to this record in ui/AGENTS.md #0.1 on the AGENTS.md |
| 113 | + pull request of [#25919](https://github.com/objectstack-ai/objectstack/issues/25919), not here. |
| 114 | +- **No new gate.** The spec's deletion gate dates its own `[RETIRED]` lines; whether the ui face gets a twin of that |
| 115 | + clock is a later card's question, decided by measurement, ⛔ not by this record. |
| 116 | + |
| 117 | +## Applying the rule |
| 118 | + |
| 119 | +The first application batch — one card per protocol major thereafter — finds due tombstones mechanically and reports |
| 120 | +what it could not date. ⛔ No count is written here: the population moves, and the instrument is the reading |
| 121 | +(ui/AGENTS.md #9). |
| 122 | + |
| 123 | +1. **Enumerate.** Every `?: never` member under `ui/packages/*/src`, tests excluded |
| 124 | + (`git grep -nE '\?: never' -- 'ui/packages/*/src/**' ':!**/__tests__/**' ':!**/*.test.ts' ':!**/*.test.tsx'`), and, |
| 125 | + per member, its zod twin on the mirrored schema, whatever helper of `zod/tombstone.zod.ts` spells it. |
| 126 | +2. **Date.** Per member, find the commit that introduced the tombstone with `git log -S'<the member line>' --reverse` |
| 127 | + on a **full** clone, then the first release tag that contains it (`git tag --contains`). The ui tree's history before |
| 128 | + the ui merge was not imported into this repository: a tombstone older than the merge is dated on a full clone of |
| 129 | + `objectstack-ai/objectui`, whose `@object-ui/types@*` tags carry that era. On a shallow clone the reading is refused, |
| 130 | + ⛔ never read as "no history" (`git rev-parse --is-shallow-repository`, with a control leg of known age). For a |
| 131 | + tombstone that names a spec retirement, read R from the spec (D2), not from the ui commit. |
| 132 | +3. **Judge.** Due ⇔ the current protocol major ≥ R + 2 and D3 does not hold the key. Report three lists — due, held by |
| 133 | + D3 (with the container), undated — and remove only the first. |
| 134 | +4. **Remove and re-pin.** Delete the member and its JSDoc; delete the zod twin where D3 allows; move any test that pins |
| 135 | + the tombstone's named refusal to assert what the key meets now — the TS excess-property refusal and the strict zod |
| 136 | + face's unknown-key refusal — ⛔ never delete such a pin without its replacement. Ship the D4 changeset. |
| 137 | + |
| 138 | +## Consequences |
| 139 | + |
| 140 | +- The ui authoring face stops only growing: each major sheds the tombstones whose window closed, in step with the |
| 141 | + spec's own face, and the two faces never disagree about how long a remedy is printed. |
| 142 | +- An author who writes a key after its window meets a bare unknown-key refusal on the TS face, without a remedy; the |
| 143 | + remedy lives in the CHANGELOG entry the D4 Breaking note writes. That is the trade ADR-0087 already made for the spec. |
| 144 | +- D3 keeps some tombstones past their window, by design: a named refusal outranks a shorter face. Each one held is a |
| 145 | + pointer at a container that is still open. |
| 146 | + |
| 147 | +## Alternatives considered |
| 148 | + |
| 149 | +- **B — leave immediately when the spec retires the key.** Rejected by the ruling: the remedy disappears while authors |
| 150 | + on the previous major still need it. |
| 151 | +- **C — no rule.** Rejected by the ruling: every retirement is a permanent obligation on the published face. |
| 152 | +- **A longer ui window than the spec's.** Excluded by the ruling's "never a longer one": the faces would print a remedy |
| 153 | + for a key the spec no longer prescribes. |
0 commit comments