diff --git a/.changeset/7678-tombstone-discriminator-agreement.md b/.changeset/7678-tombstone-discriminator-agreement.md new file mode 100644 index 0000000000..0a2278101d --- /dev/null +++ b/.changeset/7678-tombstone-discriminator-agreement.md @@ -0,0 +1,45 @@ +--- +--- + +Comment-only agreement fix in `@object-ui/types`. The retire-vs-remove discriminator was +stated at three sites in `complex.ts` at two different levels of amendment: the +`KanbanColumn.color` block and the `ChatbotSchema` dark-keys block gave the two prongs +without their precondition, while the `displayMode` tombstone already carried it. Both of +the first two now state the amended rule (objectui#7678) in the wording the third already +uses — a `?: never` tombstone is available only on a surviving carrier, and on one it is +used when either prong holds; a whole exported type name has no carrier and is removed +outright — so a reader landing on any of the three gets the same rule. The +`KanbanColumn.color` site's own parenthetical had named this card as its open correction; +that parenthetical is now discharged rather than repeated. + +The `triggerIcon` tombstone's own rationale was the site objectui#7678 was filed about, and +it still argued from `tsc` behaviour alone — a contrast that applied equally to the two +precedent retirements that were removed outright, so it cannot be what separates the routes. +It now carries the amended discriminator and names prong 2 (the 3.3.0 release record +advertised the key, and its published JSDoc promised a default) as the reason this key earns +a tombstone, with prong 1 recorded as not holding. The route itself is unchanged. + +`mobile.ts`'s `MobileOverrides` retirement note keeps its prong-1 argument, which is +settled, and gains a pointer to the amended rule and to the reason prong 2 has nothing to +guard there: this module has never had a `zod/` twin, so there is no mirror that could +accept an undeclared key and strip it silently. + +`mobile.ts`'s `GestureConfig` retirement note stated the two prongs in full and without the +precondition. It now carries the amended form in the same wording as the other sites, and +records that the precondition settles the route there on its own, a whole exported type name +having no carrier to host a `?: never` member. Every existing clause is kept, including the +per-prong measurement and the note that whether a reclaim line counts as "taught as working" +is recorded on objectui#7519 rather than decided there. The removal is not re-argued. + +The `objectui#6152` tripwire comment in `floating-chatbot-trigger-icon-retired.test.ts` +claimed the parse-green assertion it sits on would go red once a `FloatingChatbotConfigSchema` +is minted. Measured by injecting the arm on both twins and restoring under a trap: a +house-style non-strict `z.object` mirror reds the shape pin only (2 failures, one per twin) +and leaves that parse-green line green, while a `z.strictObject` mirror reds both (4 +failures). The comment now names the shape pin as the assertion that fires and records both +readings; the shape pin gained a back-pointer. + +`Clause-②: no` — no member added, removed, renamed or retyped, no accept set moved, and +nothing newly exported. The prose does reach the emitted `.d.ts` (this repo builds with +`removeComments: false`), but nothing a consumer can execute or type-check against changes, +so this releases nothing. diff --git a/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts b/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts index 369e41b52c..c9ac1af398 100644 --- a/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts +++ b/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts @@ -152,11 +152,19 @@ describe.each([ // green before the tombstone and is green after it — the retirement changed // the TypeScript face only, and this pins that it changed no parse outcome. // - // ⚠️ TRIPWIRE: if objectui#6152 ever mints a `FloatingChatbotConfigSchema`, - // this goes RED. That is the intended signal, not a nuisance — whoever - // lands the mirror must add the `retirementTombstone()` half for - // `triggerIcon` at the same time, and flip this control rather than delete - // it into a vacuum. + // ⚠️ TRIPWIRE: if objectui#6152 ever mints a `FloatingChatbotConfigSchema` + // and wires it onto these twins as the `floatingConfig` arm, the assertion + // that fires is the SHAPE PIN at the foot of this block — the one reading + // `shape.floatingConfig` — and NOT this line. Measured on objectui#7678's + // base, arm injected on both twins and restored under a trap: a + // house-style non-strict `z.object` mirror reds the shape pin ONLY (2 + // failures, one per twin) and leaves this parse-green line GREEN, because + // a non-strict object accepts `triggerIcon` and strips it, so `success` + // stays `true`; a `z.strictObject` mirror reds both (4 failures). Either + // shape trips the file — that is the intended signal, not a nuisance: + // whoever lands the mirror must add the `retirementTombstone()` half for + // `triggerIcon` at the same time, and flip these controls rather than + // delete them into a vacuum. const result = twin.safeParse({ ...node, floatingConfig: { title: 'Chat', triggerIcon: 'Sparkles' }, @@ -175,6 +183,10 @@ describe.each([ it('the mirror really has no `floatingConfig` key at all', () => { // The load-bearing fact behind everything above, asserted rather than // assumed: a key the mirror declares would appear in its shape. + // + // ⚠️ This is also the assertion the objectui#6152 TRIPWIRE fires through: + // under a house-style non-strict mirror it reds HERE and nowhere else in + // this file. The measurement is recorded at that comment, above. const shape = (twin as unknown as { shape: Record }).shape; expect(shape.floatingConfig).toBeUndefined(); // Lit control: a key the mirror DOES declare is present, so the reading diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index fb91b20628..5058cc9fab 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -156,10 +156,13 @@ export interface KanbanColumn { * `KanbanEnhanced`, `ObjectKanban`). * * A tombstone rather than a plain removal on BOTH prongs of the - * discriminator the precedent changesets state (objectui#5941, #7526; the - * one-line form is under correction as objectui#7678) — a tombstone exists - * (1) to steer authors to a named live replacement KEY, or (2) to keep loud a - * key the docs taught as working: + * discriminator the precedent changesets state (objectui#5941, #7526), in + * the form objectui#7678 amended it to: a `?: never` tombstone is available + * only on a SURVIVING CARRIER — `KanbanColumn` survives this retirement, + * while a whole exported type name has no carrier and is removed outright — + * and on such a carrier it is used when either prong holds: (1) it steers + * authors to a named live replacement KEY, or (2) it keeps loud a key the + * docs taught as working. Both hold here: * * - prong 1: `className` is that live replacement — style a lane through * it; @@ -1054,9 +1057,12 @@ export interface ChatToolInvocation { * UNDECLARED key is not refused, it is KEPT. That is the hazard the two-prong * discriminator leaves to the carrier — where there is no mirror there is "no * silent-strip hazard for prong 2 to guard" (`mobile.ts`, objectui#5941 / - * #7526 / #7678: a tombstone exists to steer authors to a named live - * replacement KEY, or to keep loud a key the docs taught as working). Here - * there IS a mirror to host the refusal, and prong 1 holds by the letter for + * #7526 / #7678: a `?: never` tombstone is available only on a SURVIVING + * CARRIER — `ChatbotSchema` survives, while a whole exported type name has no + * carrier and is removed outright — and on such a carrier it is used when + * either prong holds: it steers authors to a named live replacement KEY, or it + * keeps loud a key the docs taught as working). Here there IS a mirror to host + * the refusal, and prong 1 holds by the letter for * four of the six — `userAvatar` → `userAvatarUrl`, `assistantAvatar` → * `assistantAvatarUrl`, `height` → `maxHeight`, `markdown` → * `enableMarkdown` on a `chatbot-enhanced` node. Each member's own comment @@ -1864,6 +1870,22 @@ export interface FloatingChatbotConfig { * - TOMBSTONED, both paths are refused: the declared `never` makes the * assignment itself ill-typed, so freshness stops mattering. * + * ⚠️ That `tsc` contrast is the MECHANISM, not the discriminator. It applied + * equally to objectui#4919 and #5942, which were removed outright, so it + * cannot be what separates the routes. What decides the route is this + * package's retire-vs-remove discriminator in its amended form + * (objectui#7678, the same wording the `chatbot` tombstones above carry): a + * `?: never` tombstone is available only on a SURVIVING CARRIER — + * `FloatingChatbotConfig` survives this retirement, while a whole exported + * type name has no carrier and is removed outright — and on such a carrier + * it is used when either prong holds. PRONG 2 is the one that holds here: + * the key was advertised in the 3.3.0 release record (2026-04-17, the "New + * ChatbotSchema floating fields" entry, which names `triggerIcon` among + * `FloatingChatbotConfig`'s options) and its published JSDoc promised + * `@default 'MessageCircle'` — the docs taught it as working. Prong 1 does + * NOT hold: there is no replacement key, which is why the guidance below + * names the trigger's own markup instead. + * * Pinned in `__tests__/floating-chatbot-trigger-icon-retired.test.ts`, * including the deletion contrast, so nobody can "simplify" this back into a * deletion without that file going red. diff --git a/packages/types/src/mobile.ts b/packages/types/src/mobile.ts index 7f291a61b0..5e3ad63dfb 100644 --- a/packages/types/src/mobile.ts +++ b/packages/types/src/mobile.ts @@ -133,6 +133,19 @@ export type ResponsiveValue = T | Partial>; // the retired `AccordionItem.icon` / `ToggleGroupItem.icon`, which were // likewise removed outright rather than tombstoned. // +// That one line states prong 1 alone. The discriminator's full, amended form +// (objectui#7678) is stated in `complex.ts` beside the `chatbot` tombstones: a +// `?: never` tombstone is available only on a SURVIVING CARRIER, and on one it +// is used when EITHER prong holds — a named live replacement KEY, or keeping +// loud a key the docs taught as working. Neither branch reaches this +// retirement: an exported type name that goes whole has no carrier to host a +// `?: never` member at all, and where a member did have one, prong 1 fails +// (above) and prong 2 has nothing to guard — this module has never had a +// `zod/` twin, so there is no mirror that could accept an undeclared key and +// strip it silently, which is the hazard prong 2 guards where a mirror does +// exist. ⛔ The route itself is settled (objectui#4919); this note states the +// rule it followed and does not reopen it. +// // Reopen condition, recorded on objectui#4919: real mobile-override renderer // work re-enters as designed product surface on its own card, with the // renderer landing in the same change as the declaration. Re-adding the @@ -289,10 +302,15 @@ export type GestureType ='tap' | 'double-tap' | 'long-press' | 'swipe-left' | 's // A binding written against it could not reach a handler by any path — // `action` was a string nothing dispatched. // -// Removed outright rather than tombstoned, measured against the two-prong -// discriminator the precedent changesets state (objectui#5941, #7526): a -// tombstone exists (1) to steer authors to a named live replacement KEY, or -// (2) to keep loud a key the docs taught as working. Prong 1: none — no +// Removed outright rather than tombstoned, measured against the discriminator +// the precedent changesets state (objectui#5941, #7526), in the form +// objectui#7678 amended it to: a `?: never` tombstone is available only on a +// SURVIVING CARRIER, and on such a carrier it is used when either prong holds +// — (1) it steers authors to a named live replacement KEY, or (2) it keeps +// loud a key the docs taught as working. `GestureConfig` is a whole exported +// type name, so it has no carrier to host a `?: never` member at all and the +// precondition settles the route on its own; the per-prong measurement that +// follows was taken anyway and is kept as the record. Prong 1: none — no // dispatcher reads a gesture `action` (zero hits), and the only `gestures` key // on any type is `TouchInteraction.gestures: SpecGestureConfig[]`, a different // contract with no reader of its own. Prong 2: the CHANGELOG lines naming it