diff --git a/.changeset/5905-componentinput-inputtype-tombstone.md b/.changeset/5905-componentinput-inputtype-tombstone.md new file mode 100644 index 0000000000..f2ef359a70 --- /dev/null +++ b/.changeset/5905-componentinput-inputtype-tombstone.md @@ -0,0 +1,64 @@ +--- +'@object-ui/types': minor +'@object-ui/plugin-markdown': minor +--- + +Retire `ComponentInput.inputType` — the fifth and last key objectui#5905 named (ADR-0049 +enforce-or-remove, maintainer ruling 2026-08-31, option B). + +`inputType` was held back when `min` / `max` / `step` / `placeholder` were retired, because +its defect was a different one. Those four were declared-and-UNREAD. `inputType` was +declared-and-DROPPED: the repository really did author it — `packages/plugin-markdown` +wrote `inputType: 'textarea'` on its `content` input, pinned by that package's own test — +while the manifest serializer dropped it. Retiring it therefore had to decide what that +registration should say instead, which is the fork the card reported and the ruling closed. + +FROM → TO: + +- `inputType?: string` → **tombstoned** (`?: never` on the interface, `retirementTombstone()` + named refusal on the Zod mirror). Put the control hint in `description`, which IS + published. +- `plugin-markdown`'s `inputType: 'textarea'` write → **deleted**, at zero capability cost. + +The write was measured as a no-op before it was deleted, and re-measured on this branch's +base rather than inherited from the card. A structural census over every `inputs:` array in +the repository (211 regions, all tracked TS/TSX/JS sources) scores `inputType` at exactly +ONE authoring site — the `plugin-markdown` registration — against `name` 953, `type` 969, +`label` 966, `description` 194, `enum` 119, `required` 86 and `binding` 4 in the same pass +over the same regions, so the instrument was not blind. The other 192 in-repo `inputType` +hits are a DIFFERENT face: `FormField.inputType` (`zod/form.zod.ts`), the text-input +renderer's prop, and `SchemaBuilder.inputType`, none of which sit on a `ComponentInput`. +The publication path is unchanged and was re-confirmed: `packages/sdui-parser/src/index.ts` +forwards exactly six keys per input — `name`, `type`, `required`, `enum`, `binding`, +`description` — so an authored `inputType` could not reach the published +`sdui.manifest.json` even in principle. + +Option A — teach `sdui-parser` to forward the key — is REFUSED on record. The only thing +that looked like demand for it was a write that had never taken effect, and a write nothing +reads is not demand for a feature. The neighbouring 2026-08-17 expression-ceiling ruling +(quoted on `ComponentInput.type`) is untouched and stays deferred, with its reopen +condition — a measured case of an author shipping a spec-rejected value objectui's silence +let through — unchanged. + +Deleting the member outright was again the option NOT taken, for the reason the four +siblings established: `ComponentInputSchema` is a non-strict `z.object`, so an undeclared +key is silently STRIPPED. The tombstone is what converts a write from OUTSIDE this +repository — the half objectui#5905 could not measure — into a named refusal carrying its +own remedy, with `code: 'invalid_type'` and the key named in the issue `path`. + +Accept-set change, stated plainly for reviewers: a document that sets `ComponentInput.inputType` +used to parse GREEN (the value was then dropped by the serializer) and now parses RED. That +is the intended effect and the reason this carries a contract-review label. + +Three pins were FLIPPED rather than deleted, so the closure stays asserted instead of +becoming a silent absence: `plugin-markdown`'s `index.test.ts` (which asserted the write) +now asserts the key's absence plus a `tsc` refusal at that package's own authoring site, +and the two fork-half controls in +`packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts` — one +type-level, one parse-level — now assert refusal where they asserted liveness. + +Stale wording corrected in the same pass, because this change falsifies it: `base.ts` and +`zod/base.zod.ts` both said the fork was "recorded for a ruling; until then this stays a +live, writable key", and `widget.ts` called it "the open fork". All three now record the +ruling. A reader who greps the source instead of the card thread was meeting an open fork +that no longer existed. diff --git a/.changeset/5905-componentinput-retire-constraint-keys.md b/.changeset/5905-componentinput-retire-constraint-keys.md index b5fe614c7b..cca4c4d014 100644 --- a/.changeset/5905-componentinput-retire-constraint-keys.md +++ b/.changeset/5905-componentinput-retire-constraint-keys.md @@ -17,11 +17,16 @@ and `placeholder` **0**, against `name` 926, `type` 926, `description` 161, `enu FROM → TO, per key: -- `min: number` → **removed**. Spell the numeric domain out in `description`, which IS - published (`'A positive integer — the contract rejects 0 and fractional values'`). -- `max: number` → **removed**. Same remedy. -- `step: number` → **removed**. Same remedy. -- `placeholder: string` → **removed**. Put the hint in `description`. ⚠️ +⚠️ The four are **TOMBSTONED, not removed** — the declaration stays, the key becomes +unwritable. An earlier draft of this list said "removed", which contradicted the paragraph +below it and described the option this change deliberately did NOT take. + +- `min: number` → **tombstoned** (`?: never`, named Zod refusal). Spell the numeric domain + out in `description`, which IS published (`'A positive integer — the contract rejects 0 + and fractional values'`). +- `max: number` → **tombstoned**. Same remedy. +- `step: number` → **tombstoned**. Same remedy. +- `placeholder: string` → **tombstoned**. Put the hint in `description`. ⚠️ `BaseSchema.placeholder` — the node-level prop a renderer does read — is a DIFFERENT key and is unaffected. @@ -42,7 +47,9 @@ Two limits worth stating rather than papering over: is exactly what the tombstone buys. - The fifth key objectui#5905 named, `inputType`, is **NOT retired here**. `packages/plugin-markdown` authors it (`inputType: 'textarea'`), so it is - declared-and-DROPPED — a different defect that needs a ruling, not a removal. + declared-and-DROPPED — a different defect that needs a ruling, not a removal. That + ruling landed on 2026-08-31 and `inputType` is tombstoned in the follow-up change; this + note records the state as of THIS change, which is what a changeset is for. This is not a verdict that constraint slots on `ComponentInput` were a mistake. The neighbouring `type` field carries a maintainer ruling of 2026-08-17 recording that giving diff --git a/packages/plugin-markdown/src/index.test.ts b/packages/plugin-markdown/src/index.test.ts index 381b188ae8..50978070de 100644 --- a/packages/plugin-markdown/src/index.test.ts +++ b/packages/plugin-markdown/src/index.test.ts @@ -8,6 +8,7 @@ import { describe, it, expect } from 'vitest'; import { ComponentRegistry } from '@object-ui/core'; +import type { ComponentInput } from '@object-ui/types'; // Imports all renderers to register them. Module scope, NOT awaited inside a // `beforeAll` — there the cold transform of the renderer graph is billed to the // hook, against `hookTimeout`. That is what made the sibling plugin-kanban test @@ -48,7 +49,40 @@ describe('Plugin Markdown', () => { expect(contentInput).toBeDefined(); expect(contentInput?.required).toBe(true); expect(contentInput?.type).toBe('string'); - expect(contentInput?.inputType).toBe('textarea'); + }); + + it('no longer authors `inputType` — the write was a measured no-op (objectui#5905)', () => { + // FLIPPED, not deleted. This assertion used to read + // `expect(contentInput?.inputType).toBe('textarea')`, and it pinned the + // ONLY `ComponentInput.inputType` write in the repository. The manifest + // serializer forwards six keys — `name`, `type`, `required`, `enum`, + // `binding`, `description` — and this was never one of them, so the + // write could not reach the published `sdui.manifest.json` even in + // principle, and a structural census over every `inputs:` array found no + // reader either. Maintainer ruling 2026-08-31 (objectui#5905) deleted + // the write and tombstoned the key. Restated here rather than removed, + // so the deletion stays asserted instead of becoming a silent absence. + const config = ComponentRegistry.getConfig('markdown'); + const contentInput = config?.inputs?.find((input: any) => input.name === 'content'); + + expect(contentInput).toBeDefined(); + expect(contentInput?.inputType).toBeUndefined(); + }); + + it('and re-authoring `inputType` is a `tsc` error at this package\'s own site', () => { + // REAL enforcement, not decoration: this package's `type-check` script + // runs `tsc -p tsconfig.test.json`, so the directive below is evaluated + // and an UNUSED one fails the build. Re-widening + // `ComponentInput.inputType` therefore turns this line red instead of + // quietly letting the no-op write back in. + const reAuthored: ComponentInput = { + name: 'content', + type: 'string', + // @ts-expect-error `inputType` is an ADR-0049 retirement tombstone (objectui#5905) + inputType: 'textarea', + }; + + expect(reAuthored.name).toBe('content'); }); it('should have sensible default props', () => { diff --git a/packages/plugin-markdown/src/index.tsx b/packages/plugin-markdown/src/index.tsx index d28f65192c..c1ea470c59 100644 --- a/packages/plugin-markdown/src/index.tsx +++ b/packages/plugin-markdown/src/index.tsx @@ -56,8 +56,7 @@ ComponentRegistry.register( name: 'content', type: 'string', label: 'Markdown Content', - required: true, - inputType: 'textarea' + required: true }, { name: 'className', type: 'string', label: 'CSS Class' } ], diff --git a/packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts b/packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts index 11ecf9ebc4..6de688af12 100644 --- a/packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts +++ b/packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts @@ -7,9 +7,15 @@ */ /** - * `ComponentInput`'s four inert constraint keys are ADR-0049 RETIREMENT + * `ComponentInput`'s five inert authoring keys are ADR-0049 RETIREMENT * TOMBSTONES, and the refusal is LOUD (objectui#5905). * + * (The file name says "constraint keys" because the first four were retired + * together and all four read like constraint slots. The fifth, `inputType`, is + * a control-kind hint rather than a constraint; it joined them later and by its + * own ruling — see the section below. The name is kept so the file's history + * stays greppable.) + * * ## What was measured * * `min` / `max` / `step` / `placeholder` were declared on `ComponentInput` and @@ -40,15 +46,26 @@ * plus the CONTRAST against a genuinely undeclared key, so nobody can "simplify" * the tombstones into deletions without this file going red. * - * ## `inputType` is NOT here, deliberately + * ## `inputType` IS here now — the fork closed, and how + * + * The fifth key objectui#5905 named used to be live and writable, because the + * repository AUTHORED it: `packages/plugin-markdown/src/index.tsx` declared + * `inputType: 'textarea'` on its `content` input. That was + * declared-and-DROPPED — a different defect from the declared-and-unread four + * — so it needed a ruling rather than a removal, and its liveness was pinned + * here to keep the fork visible. + * + * The ruling landed: maintainer, 2026-08-31 (objectui#5905, director seat + * summon 6, decision batch #14), option B. The write was measured as a NO-OP + * (the serializer dropped it; a structural census over every `inputs:` array + * found no reader), so it was DELETED at zero capability cost and the key + * joined the tombstones. Option A — teach `sdui-parser` to forward it — is + * REFUSED on record: a write nothing reads is not demand for a feature. * - * The fifth key objectui#5905 named is still live and still writable, because - * the repository AUTHORS it: `packages/plugin-markdown/src/index.tsx` declares - * `inputType: 'textarea'` on its `content` input (pinned by that package's own - * test). That is declared-and-DROPPED — a different defect from the - * declared-and-unread four — and it needs a ruling, not a removal. Its liveness - * is pinned below so the fork stays visible and closing it stays a deliberate - * edit to this file. + * The two controls that pinned the fork's LIVE half are FLIPPED below rather + * than deleted (`keeps inputType WRITABLE` and `inputType still parses green`). + * A control is restated on success, never deleted into a vacuum — deleting them + * would leave the closure asserted by nothing at all. * * The `@ts-expect-error` directives are REAL enforcement: this package * type-checks its tests through `tsconfig.test.json`, so re-widening the @@ -59,12 +76,16 @@ import { describe, it, expect } from 'vitest'; import type { ComponentInput } from '../base'; import { ComponentInputSchema } from '../zod/base.zod'; -/** The four retired keys, with a value an author would plausibly have written. */ +/** The five retired keys, with a value an author would plausibly have written. */ const RETIRED = { min: 0, max: 100, step: 1, placeholder: 'Type here…', + // Retired later than the four above, by its own ruling (2026-08-31). The + // value is the exact one `plugin-markdown` used to author, so the loop below + // exercises the real historical write rather than an invented one. + inputType: 'textarea', } as const; type RetiredKey = keyof typeof RETIRED; @@ -103,12 +124,24 @@ describe('the interface tombstones make authoring a `tsc` error', () => { expect(input.name).toBe('content'); }); - it('keeps `inputType` WRITABLE — the fork objectui#5905 reported, not an oversight', () => { - // No `@ts-expect-error`: `plugin-markdown` authors this key today, so - // retiring it is a ruling about that registration, not a cleanup. If this - // line ever needs a directive, the fork was closed — say so on the card. - const input: ComponentInput = { name: 'content', type: 'string', inputType: 'textarea' }; - expect(input.inputType).toBe('textarea'); + it('refuses `inputType` too — the fork objectui#5905 reported is now CLOSED', () => { + // FLIPPED, not deleted. This control used to carry NO directive and assert + // `input.inputType === 'textarea'`, pinning the key's liveness while the + // fork was open. Its own comment named the trigger: "If this line ever + // needs a directive, the fork was closed — say so on the card." The + // maintainer closed it on 2026-08-31 (objectui#5905, option B), so the + // directive is here and this is that saying-so. + // + // The directive IS the assertion: this package type-checks its tests + // through `tsconfig.test.json`, so re-widening the declaration fails the + // build on the now-unused directive. + const input: ComponentInput = { + name: 'content', + type: 'string', + // @ts-expect-error `inputType` is a retirement tombstone (objectui#5905, ruled 2026-08-31) + inputType: 'textarea', + }; + expect(input.name).toBe('content'); }); }); @@ -126,10 +159,18 @@ describe('the zod tombstones REFUSE, loudly (objectui#5905)', () => { } }); - it('`inputType` still parses green — the fork half of the same control', () => { + it('`inputType` now parses RED — the same fork half, restated on the ruling', () => { + // FLIPPED, not deleted: this asserted `success === true` while the fork was + // open. The parameterised loop below covers `inputType` as well, now that + // it is in `RETIRED`; this case is kept because it is the one a reader + // diffs against the old file to see the fork close. const result = ComponentInputSchema.safeParse({ ...LIVE_INPUT, inputType: 'textarea' }); - expect(result.success).toBe(true); - if (result.success) expect(result.data.inputType).toBe('textarea'); + expect(result.success).toBe(false); + if (!result.success) { + const issue = result.error.issues.find((i) => String(i.path[0]) === 'inputType'); + expect(issue?.code).toBe('invalid_type'); + expect(issue?.message).toContain('RETIRED (objectui#5905)'); + } }); for (const key of Object.keys(RETIRED) as RetiredKey[]) { @@ -188,7 +229,7 @@ describe('a tombstone is not a deletion — the contrast, measured in one run', if (result.success) expect(result.data).not.toHaveProperty('notAKeyAtAll'); }); - it('the four stay in the mirror\'s shape — a tombstone is DECLARED, just unwritable', () => { + it('the five stay in the mirror\'s shape — a tombstone is DECLARED, just unwritable', () => { for (const key of Object.keys(RETIRED)) { expect(shapeOf(ComponentInputSchema)).toHaveProperty(key); expect(describeOf(ComponentInputSchema, key)).toContain('RETIRED (objectui#5905)'); diff --git a/packages/types/src/base.ts b/packages/types/src/base.ts index 2e57dcc18e..a93b73789f 100644 --- a/packages/types/src/base.ts +++ b/packages/types/src/base.ts @@ -601,25 +601,35 @@ export interface ComponentInput { advanced?: boolean; /** - * Specific input type (e.g., 'email', 'password' for string) + * RETIRED (objectui#5905, ADR-0049) — never read, and never published: the + * manifest serializer forwards six keys and this is not one of them. Put the + * control hint in `description`, which IS published. + * + * The LAST of the five to be retired, and by its own ruling, because its + * defect was a different one. The four below were declared-and-UNREAD; this + * key was declared-and-DROPPED — the repository really did author it, on + * `packages/plugin-markdown`'s `content` input — so retiring it had to decide + * what that registration should say instead, which is why it was held back + * as a fork rather than retired alongside them. + * + * Maintainer ruling 2026-08-31 (objectui#5905, director seat summon 6, + * decision batch #14) took option B: DELETE the write. It was measured as a + * no-op — the serializer dropped it, and a structural census over every + * `inputs:` array in the repository found no reader — so deleting it costs + * zero capability. Option A, teaching `sdui-parser` to forward the key, is + * REFUSED on record: a write nothing reads is not demand for a feature. The + * 2026-08-17 expression-ceiling ruling quoted on `type` above is untouched + * by this and stays deferred, reopen condition unchanged. * - * ⚠️ NOT retired alongside the four tombstones below (objectui#5905), and the - * difference is measured rather than stylistic. `plugin-markdown`'s - * registration AUTHORS this key — `inputs: [{ name: 'content', …, inputType: - * 'textarea' }]` in `packages/plugin-markdown/src/index.tsx`, pinned by that - * package's own test — while the manifest serializer still drops it. That is - * declared-and-DROPPED, a different defect from the declared-and-unread four - * below: retiring it would convert one registration's silent no-op into a - * build failure without first deciding what that registration should say - * instead (delete the line, or teach the publication path to carry it). The - * fork is recorded on objectui#5905 for a ruling; until then this stays a - * live, writable key that nothing publishes. + * @deprecated Not part of `ComponentInput`'s contract — the value was inert. */ - inputType?: string; + inputType?: never; /** * ADR-0049 RETIREMENT TOMBSTONES — `min` / `max` / `step` / `placeholder` - * (objectui#5905). + * (objectui#5905). `inputType`, declared directly above, is a FIFTH tombstone + * of exactly this shape; it carries its own block because it was retired + * later, by its own ruling, out of a different defect. * * `?: never` is this package's tombstone convention (see `crud.ts` `confirm` * and {@link StaticTableColumn} in `data-display.ts`): the key stays diff --git a/packages/types/src/widget.ts b/packages/types/src/widget.ts index 85dd8a9ad6..1895bee386 100644 --- a/packages/types/src/widget.ts +++ b/packages/types/src/widget.ts @@ -244,15 +244,20 @@ export interface WidgetSourceRegistry { * these. Copying them here would mirror surface that nothing reads on the * face it already lives on. * - * ⚠️ FOUR of those five are now ADR-0049 RETIREMENT TOMBSTONES on - * `ComponentInput` (`min` / `max` / `step` / `placeholder` — `?: never` plus - * a named Zod refusal, objectui#5905), so what this clause records is no - * longer "five keys this face declines to copy" but ONE live key - * (`inputType`) plus four unwritable ones. Copying any of them here is now - * doubly wrong: the four are REFUSED on the face they already live on, and - * `inputType` is the open fork objectui#5905 reported — `plugin-markdown` - * authors it and the serializer still drops it, which is a ruling to make, - * not a surface to mirror. + * ⚠️ ALL FIVE are now ADR-0049 RETIREMENT TOMBSTONES on `ComponentInput` + * (`inputType` / `min` / `max` / `step` / `placeholder` — `?: never` plus a + * named Zod refusal, objectui#5905), so what this clause records is no + * longer "five keys this face declines to copy" but five keys that are + * UNWRITABLE on the face they already live on. Copying any of them here + * would mirror a refusal. + * + * `inputType` was the last of the five and took its own ruling, because it + * was the one the repository actually authored: `plugin-markdown` wrote it + * while the serializer dropped it. Maintainer, 2026-08-31 (objectui#5905): + * delete the write — measured a no-op, so zero capability lost — and + * tombstone the key; teaching `sdui-parser` to forward it was REFUSED on + * record. ⇒ The fork this block used to report as OPEN is CLOSED; do not + * re-read it as a pending question. * * Pin: `__tests__/widget-input-control-vocabulary.test.ts`. */ diff --git a/packages/types/src/zod/base.zod.ts b/packages/types/src/zod/base.zod.ts index 3e65ece460..df31ace4ab 100644 --- a/packages/types/src/zod/base.zod.ts +++ b/packages/types/src/zod/base.zod.ts @@ -314,16 +314,27 @@ export const ComponentInputSchema = z.object({ description: z.string().optional().describe('Help text'), advanced: z.boolean().optional().describe('Advanced option flag'), /** - * ⚠️ NOT retired with the four tombstones below (objectui#5905): - * `plugin-markdown`'s registration authors it, so it is declared-and-DROPPED - * rather than declared-and-unread. See `ComponentInput.inputType` in - * `../base.ts` for the fork and what a ruling on it has to decide. + * ADR-0049 RETIREMENT TOMBSTONE (objectui#5905) — the FIFTH key, retired + * later than the four below and by its own ruling (maintainer, 2026-08-31). + * It was declared-and-DROPPED rather than declared-and-unread: + * `plugin-markdown`'s registration really did author it while the serializer + * dropped it, so retiring it meant ruling on that registration first. The + * ruling deleted the write as a measured no-op and REFUSED teaching + * `sdui-parser` to forward the key. See `ComponentInput.inputType` in + * `../base.ts` for the full record. */ - inputType: z.string().optional().describe('Specific input type'), + inputType: retirementTombstone( + 'RETIRED (objectui#5905) — `ComponentInput.inputType` was never read, and never published: the manifest ' + + 'serializer forwards `name`/`type`/`required`/`enum`/`binding`/`description` and this is not one of them, ' + + 'so an authored value was silently dropped. Delete the key; put the control hint in `description`, ' + + 'which IS published.', + ), /** * ADR-0049 RETIREMENT TOMBSTONES (objectui#5905) — `min` / `max` / `step` / * `placeholder`, the four `ComponentInput` keys measured with no reader on - * either the consumption or the publication path. + * either the consumption or the publication path. `inputType` directly above + * is a fifth tombstone of the same shape, added by a later ruling; everything + * this block says about the mechanism applies to it too. * * `retirementTombstone()` (`./tombstone.zod.ts`) writes each guidance string * ONCE into both author-facing channels — the parse-time issue message and