Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .changeset/5905-componentinput-inputtype-tombstone.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 13 additions & 6 deletions .changeset/5905-componentinput-retire-constraint-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down
36 changes: 35 additions & 1 deletion packages/plugin-markdown/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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', () => {
Expand Down
3 changes: 1 addition & 2 deletions packages/plugin-markdown/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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');
});
});

Expand All @@ -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[]) {
Expand Down Expand Up @@ -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)');
Expand Down
38 changes: 24 additions & 14 deletions packages/types/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading