Skip to content

Commit 27c0cf3

Browse files
fix(spec): the object-kanban quickAdd retirement prescribes "delete the key", not the unregistered kanban-ui block (#21141)
Fixes #21017 Clause-②: no The `object-kanban` `quickAdd` retirement told authors the control "is unchanged on the `kanban-ui` block". objectui retired that block (objectui#8257) and this spec never declared it as a component type, so a node written there saves clean and resolves no renderer. Every author-facing copy of the remedy now says one sentence: **Delete the key; `object-kanban` offers no quick-add control.** The refusal itself is unchanged: same key, same `invalid_type` code at path `quickAdd`, same accept set. Prose only. ## The copies | Copy | File | Change | |:--|:--|:--| | tombstone message (shown at the save door and in the generated reference) | `packages/spec/src/ui/component.zod.ts` | the sentence | | D2 conversion `object-kanban-quick-add-removed` `summary` | `packages/spec/src/conversions/registry.ts` | the sentence | | D3 entry `object-kanban-quick-add-retired`: `replacement`, `reason`, `acceptanceCriteria` (what `os migrate meta` prints) | `migrations/entries/semantic/18.object-kanban-quick-add-retired.ts`, generated copy in `migrations/registry.ts` | the sentence; the second way out "move the board to a host that renders the `kanban-ui` block" is dropped | | step 18 `rationale` fragment (the paragraph the upgrade guide prints) | `migrations/registry.ts`, the hand-written region outside the generated markers | the sentence. A sixth copy the card did not list, found by the `git grep` of the order's H2 | | docblocks: two in `component.zod.ts`, two in `conversions/registry.ts`, the retired-key entry's header, the pin test's header | same files | the ruling is still quoted, now with what objectui did to the block it named | | generated | `migrations/registry.ts` (`gen:migration-registry`), `content/docs/references/ui/component.mdx` (`gen:docs`) | regenerated, never hand-typed | ## Measured before writing - **H1, is there a rendering route.** objectui at the pin `e420df310f5b` and at main `770cc5ba4d15`: 0 code lines naming `kanban-ui` under `packages` and `apps` outside tests and CHANGELOGs; 22 comment lines in 6 files, all recording the retirement. No `register` call carries the key. `ObjectKanban` passes neither half of the pair to `KanbanBoardCore`; objectui's own docblock there says "An `object-kanban` board draws no Quick Add control, whatever its document carries". The pair is read only by the exported `KanbanRenderer` React component, which a host mounts directly, so no metadata node reaches it. No route to name, so triage's sentence stands. - **H2, every copy.** Base `git grep` of `kanban-ui` under `packages/spec/src` lists the files in the order plus the step 18 rationale fragment above. After this PR no author-facing text names the block; the remaining mentions are docblocks and comments that explain why it is not named. - **H3, what PR #20990 already did.** It added the "Re-read at `e420df310`" qualification to the second docblock. That qualification is kept, re-wrapped, with the same facts and citations. The sentence just above it still said the control "stays on `kanban-ui`", contradicting it; that sentence is the one rewritten. - **The dead-node claim.** Probe on this branch: `PageComponentSchema.safeParse` of a node with type `kanban-ui` succeeds (true), and `ComponentPropsMap['object-kanban'].safeParse({ objectName: 'task', quickAdd: true })` fails with one issue, code `invalid_type`, path `quickAdd`. ## Pin `ui/component.test.ts` (`ObjectKanbanPropsSchema quickAdd is retired`): the existing refusal test now pins "Delete the key" in place of the block name, and one new test asserts that the refusal still carries `invalid_type` at `quickAdd`, that the tombstone carries the sentence, and that none of the tombstone, the D2 summary, the D3 entry's three texts and the step 18 rationale contains `kanban-ui`. The generated copies are held to their sources by `check:migration-registry`. Ablation, each leg through `scripts/ablation-replace.mjs` in wrap mode against the committed fix: the tool printed anchor 1 to 0 and the blob hash changing before the run, and `blob == HEAD` with an empty `git diff HEAD` after it. Direction in every leg was red, exactly one failing test: leg 1 the tombstone sentence reverted (assertion on the sentence), leg 2 the D2 summary reverted, leg 3 the generated semantic copy reverted, leg 4 the step 18 rationale reverted (each of the last three on `not.toContain('kanban-ui')`). The test imports the sources by relative path, so no `dist/` is in the resolution path and no rebuild is owed between legs. ## Verified at head `83d99268a` - `pnpm --filter @objectstack/spec build` exit 0; `check:generated` all 15 artifacts up to date, with `check:migration-registry`, `check:spec-changes`, `check:upgrade-guide`, `check:docs`, `check:authorable-surface` and `check:api-surface` green. `check:docs` was stale after the tombstone edit and was regenerated: the diff of `content/docs/references/ui/component.mdx` is the one sentence. - `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` over the actual diff: 112 commands. `--ran` verdict: "112 derived famil(ies) accounted for — 112 run, 0 NOT-MEASURED", every recorded exit 0. The four artifact-roster gates whose roster sits under the touched paths (`check:meta-url-spelling`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`) also exit 0. The seven gates that first answered exit 3 (workspace not built) were re-run after `turbo run build` of the closure and exit 0. - `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ui src/conversions src/migrations scripts/step18-rationale-merge.test.ts scripts/conversions-major18-merge.test.ts`: 114 files, 4190 tests passed. `pnpm --filter @objectstack/spec typecheck` exit 0. - `origin/main` was merged with `scripts/pm/os-regen-merge.sh` (main at `8368f1c00`); regenerating the migration registry afterwards changed nothing. The repo-wide lint union and the rest of the farm are CI's. ## Acceptance notes - `packages/spec/CHANGELOG.md`, the 17.5.0 entry for the retirement (about lines 1580-1629), carries the same prescription ("It stays on the `kanban-ui` block ...", and "live `kanban-ui` spelling"). It is release-owned and published, so it is not touched here; it needs an amendment in a dedicated docs-only PR. Carrier: none. - `.changeset/objectui-pin-citations-e420df310f5b.md` already says the block was retired in objectui and is correct; untouched. - No governed surface is touched (no `.claude/**`, `skills/**`, ADR or AGENTS.md). The claim records that the at-tier contract review of the `packages/spec/src/**` face is owed before enqueue. - Changeset: `patch` for `@objectstack/spec`, `Clause-②: no`. The diff widens no accept set and no public surface. --- _Generated by [Claude Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e952cff commit 27c0cf3

8 files changed

Lines changed: 109 additions & 54 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the `object-kanban` `quickAdd` retirement no longer sends authors to the `kanban-ui` block, which objectui does not register
6+
7+
Clause-②: no
8+
9+
- The refusal of `quickAdd` on `object-kanban` now ends "Delete the key; `object-kanban` offers no quick-add control." It used to say the control "is unchanged on the `kanban-ui` block". objectui retired that block (objectui#8257), so a node of that type saves clean and renders nothing. The refusal itself is unchanged: the same key is still refused, with the same code and path.
10+
- The same sentence replaces the old one in the `os migrate meta --from 17` output (the `object-kanban-quick-add-retired` entry) and in the summary of the `object-kanban-quick-add-removed` conversion. That entry no longer offers "move the board to a host that renders the `kanban-ui` block" as a second way out.
11+
- No author action beyond the existing one. `quickAdd: true` on an `object-kanban` is still a parse error. Delete the key.

‎content/docs/references/ui/component.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,7 @@ Sort field and direction pair
822822
| **cardFields** | `string[]` | optional | Fields rendered on each card |
823823
| **swimlaneField** | `string` | optional | Field for horizontal swimlanes (in addition to columns) |
824824
| **grouping** | `{ fields: object[] }` | optional | View grouping config; its first field is the swimlane fallback |
825-
| **quickAdd** | `never` | optional | [REMOVED] `object-kanban` property `quickAdd` was removed in @objectstack/spec 17 (ADR-0049) — the board forwarded it, but the per-column affordance is gated on both `quickAdd` and `onQuickAdd`, and `onQuickAdd` is a host-supplied function JSON cannot carry and no producer ever put on an `object-kanban` node, so authoring it was a parse-clean no-op. Delete the key. The quick-add control is unchanged on the `kanban-ui` block, where a React host supplies the `onQuickAdd` slot the control needs. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
825+
| **quickAdd** | `never` | optional | [REMOVED] `object-kanban` property `quickAdd` was removed in @objectstack/spec 17 (ADR-0049) — the board forwarded it, but the per-column affordance is gated on both `quickAdd` and `onQuickAdd`, and `onQuickAdd` is a host-supplied function JSON cannot carry and no producer ever put on an `object-kanban` node, so authoring it was a parse-clean no-op. Delete the key; `object-kanban` offers no quick-add control. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
826826
| **coverImageField** | `string` | optional | Image field rendered as the card cover |
827827
| **conditionalFormatting** | `any` | optional | Card conditional formatting rules |
828828
| **navigation** | `{ mode: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; preventNavigation: boolean; openNewTab: boolean; size: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Card-click navigation config — the same block `ListViewSchema.navigation` declares (`{ mode, size, openNewTab, preventNavigation }`). The renderer's own default is `{ mode: 'drawer' }` when the key is absent; it is documented rather than declared, so a parsed board carries the key only when the author wrote it |

‎packages/spec/src/conversions/registry.ts‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8929,9 +8929,11 @@ const objectGridDefaultSortRemoved: MetadataConversion = {
89298929
* `object-kanban`'s per-column quick-add switch leaves the contract (protocol
89308930
* 18, #17260, ADR-0049 enforce-or-remove; the spec half of the objectui#8285
89318931
* director-seat ruling, decision batch #91, 2026-09-08 — ruled option B,
8932-
* `quickAdd` is retired from the board and stays only on the `kanban-ui`
8933-
* block, where a React host can supply the runtime function the control
8934-
* needs).
8932+
* `quickAdd` is retired from the board. The ruling kept the control on the
8933+
* `kanban-ui` block, where a React host can supply the runtime function it
8934+
* needs; objectui has since retired that block (objectui#8257), so
8935+
* `object-kanban` offers no quick-add control and no block a document can
8936+
* name offers one either).
89358937
*
89368938
* **A pure lossless delete.** The key never had an effect to preserve.
89378939
* Measured at the `.objectui-sha` pin (`53ded82bf`): the board FORWARDS it —
@@ -8945,12 +8947,11 @@ const objectGridDefaultSortRemoved: MetadataConversion = {
89458947
* `onCardClick` in the same file). So the gate was permanently false and
89468948
* deleting the key preserves observed behaviour exactly.
89478949
*
8948-
* ⚠️ Scoped by component `type`, never by key name. `quickAdd` is LIVE on the
8949-
* `kanban-ui` block — the same renderer chain, reached by a React host that
8950-
* hands in `onQuickAdd` — and the ruling keeps it there deliberately. That
8951-
* block is objectui-side and is not a component type this spec declares, so no
8952-
* stack this walk reaches can carry it; the type scoping is what keeps the
8953-
* strip from generalising into a name-keyed one if it ever is declared. The
8950+
* ⚠️ Scoped by component `type`, never by key name, so the strip cannot
8951+
* generalise into a name-keyed one: a node of any other type that authors the
8952+
* same key is left alone. The `kanban-ui` block the ruling kept the control on
8953+
* is retired (objectui#8257) and was never a component type this spec
8954+
* declares, so no stack this walk reaches is expected to carry it. The
89548955
* fixture's non-carrier control is an `object-grid` authoring the same key
89558956
* name.
89568957
*
@@ -8973,7 +8974,7 @@ const objectKanbanQuickAddRemoved: MetadataConversion = {
89738974
summary:
89748975
"object-kanban component prop 'quickAdd' removed (#17260 — the affordance is gated on a "
89758976
+ "host-supplied 'onQuickAdd' function no producer puts on an object-kanban node, so the key "
8976-
+ "was accepted and dropped; the quick-add control stays on the React-host 'kanban-ui' block)",
8977+
+ "was accepted and dropped; delete the key — object-kanban offers no quick-add control)",
89778978
apply(stack, emit) {
89788979
return mapPageComponents(stack, (component, path) => {
89798980
if (component.type !== 'object-kanban') return component;
@@ -9001,7 +9002,7 @@ const objectKanbanQuickAddRemoved: MetadataConversion = {
90019002
// ⚠️ The same key name on a component that is NOT an
90029003
// `object-kanban` — not this entry's key. Untouched: the strip
90039004
// is scoped by component type, never by key name, which is what
9004-
// keeps the LIVE `kanban-ui` spelling out of its reach.
9005+
// keeps this key out of every other component type's reach.
90059006
{
90069007
type: 'object-grid',
90079008
id: 'g1',

‎packages/spec/src/migrations/entries/retired-keys/18.ui__ObjectKanbanProps__quickAdd.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
// #17260 — ADR-0049 enforce-or-remove, executing the objectui#8285
44
// director-seat ruling (comment 5583979207, decision batch #91, 2026-09-08,
55
// standing maintainer delegation): ruled option B — `quickAdd` is retired from
6-
// the `object-kanban` board and stays only on the `kanban-ui` block, where a
7-
// React host can supply the runtime function the control needs.
6+
// the `object-kanban` board. The ruling kept the control on the `kanban-ui`
7+
// block, where a React host can supply the runtime function it needs; objectui
8+
// has since retired that block (objectui#8257), so `object-kanban` offers no
9+
// quick-add control and no block a document can name offers one either.
810
// The board FORWARDED the key but never honoured it: measured at the
911
// `.objectui-sha` pin `53ded82bf`, `ObjectKanban.tsx:931` spreads the authored
1012
// bag into `KanbanRenderer` (`plugin-kanban/src/index.tsx:196` passes both
@@ -20,7 +22,7 @@
2022
// wrong. Tombstoned with `retiredKey()` in `ObjectKanbanPropsSchema` (the
2123
// surface baseline line carries `[RETIRED]`); sources are stripped by the D2
2224
// conversion `object-kanban-quick-add-removed`, a pure lossless delete scoped
23-
// by component `type` so the LIVE `kanban-ui` spelling is untouched.
25+
// by component `type` so the same key on any other component type is untouched.
2426
//
2527
// Registered under 18, not 17: v17.0.0 was cut before this landed, so the
2628
// removal ships on the 17.x line (launch-window convention: accept-set

‎packages/spec/src/migrations/entries/semantic/18.object-kanban-quick-add-retired.ts‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,19 @@ export const entry: SemanticMigration = {
1111
id: 'object-kanban-quick-add-retired',
1212
surface: 'page.component.object-kanban.quickAdd — the per-column quick-add switch on the '
1313
+ 'metadata-driven board',
14-
replacement: '(removed from the metadata board.) The quick-add control exists on the `kanban-ui` '
15-
+ 'block, where a React host supplies the `onQuickAdd` function the control calls. On a '
16-
+ 'metadata board, records are created through the object\'s ordinary create action.',
14+
replacement: '(removed from the metadata board.) Delete the key; `object-kanban` offers no '
15+
+ 'quick-add control. On a metadata board, records are created through the object\'s ordinary '
16+
+ 'create action.',
1717
reason: 'The D2 conversion `object-kanban-quick-add-removed` deletes `quickAdd` from every '
1818
+ '`object-kanban` component, and the delete is lossless: the board forwarded the flag, but the '
1919
+ 'control also needs a host-supplied `onQuickAdd` function that JSON cannot carry and no '
2020
+ 'producer ever put on an object-kanban node, so the gate was permanently false and no board '
2121
+ 'ever showed the control. The residue is the requirement behind the flag. An author who set '
2222
+ '`quickAdd: true` wanted users to add a card inside a column; that never happened and still '
23-
+ 'does not. Whether the board can live without it, or needs a React host rendering the '
24-
+ '`kanban-ui` block with a real `onQuickAdd`, is a product decision about that board — not '
25-
+ 'something a key delete can make.',
23+
+ 'does not. Whether the board can live without it is a product decision about that board — '
24+
+ 'not something a key delete can make.',
2625
acceptanceCriteria: 'No `object-kanban` component carries `quickAdd`; the parse refuses it. Each '
2726
+ 'board renders the same columns and cards as before the upgrade. For each board that had set '
28-
+ 'the flag, the author has either accepted creating records through the object\'s create '
29-
+ 'action, or moved that board to a host that renders the `kanban-ui` block with `onQuickAdd` '
30-
+ 'supplied — where clicking a column\'s add control creates a record in that column.',
27+
+ 'the flag, the author has accepted creating records through the object\'s create action: '
28+
+ '`object-kanban` offers no quick-add control.',
3129
};

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5709,8 +5709,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [
57095709
+ 'an author following the contract met a tool contradicting it with no way to tell which side '
57105710
+ 'was wrong. A retiredKey tombstone on `ObjectKanbanPropsSchema` with one D2 conversion that '
57115711
+ 'is a pure lossless DELETE (the key never had an effect to preserve) scoped by component '
5712-
+ '`type`: `quickAdd` stays LIVE on the `kanban-ui` block, where a React host supplies the '
5713-
+ 'runtime slot, and the ruling keeps it there deliberately.',
5712+
+ '`type`. Delete the key; `object-kanban` offers no quick-add control.',
57145713
},
57155714
{
57165715
id: 'object-tenancy-organization-field-retired',
@@ -14163,23 +14162,21 @@ const step18: MigrationStep = {
1416314162
id: 'object-kanban-quick-add-retired',
1416414163
surface: 'page.component.object-kanban.quickAdd — the per-column quick-add switch on the '
1416514164
+ 'metadata-driven board',
14166-
replacement: '(removed from the metadata board.) The quick-add control exists on the `kanban-ui` '
14167-
+ 'block, where a React host supplies the `onQuickAdd` function the control calls. On a '
14168-
+ 'metadata board, records are created through the object\'s ordinary create action.',
14165+
replacement: '(removed from the metadata board.) Delete the key; `object-kanban` offers no '
14166+
+ 'quick-add control. On a metadata board, records are created through the object\'s ordinary '
14167+
+ 'create action.',
1416914168
reason: 'The D2 conversion `object-kanban-quick-add-removed` deletes `quickAdd` from every '
1417014169
+ '`object-kanban` component, and the delete is lossless: the board forwarded the flag, but the '
1417114170
+ 'control also needs a host-supplied `onQuickAdd` function that JSON cannot carry and no '
1417214171
+ 'producer ever put on an object-kanban node, so the gate was permanently false and no board '
1417314172
+ 'ever showed the control. The residue is the requirement behind the flag. An author who set '
1417414173
+ '`quickAdd: true` wanted users to add a card inside a column; that never happened and still '
14175-
+ 'does not. Whether the board can live without it, or needs a React host rendering the '
14176-
+ '`kanban-ui` block with a real `onQuickAdd`, is a product decision about that board — not '
14177-
+ 'something a key delete can make.',
14174+
+ 'does not. Whether the board can live without it is a product decision about that board — '
14175+
+ 'not something a key delete can make.',
1417814176
acceptanceCriteria: 'No `object-kanban` component carries `quickAdd`; the parse refuses it. Each '
1417914177
+ 'board renders the same columns and cards as before the upgrade. For each board that had set '
14180-
+ 'the flag, the author has either accepted creating records through the object\'s create '
14181-
+ 'action, or moved that board to a host that renders the `kanban-ui` block with `onQuickAdd` '
14182-
+ 'supplied — where clicking a column\'s add control creates a record in that column.',
14178+
+ 'the flag, the author has accepted creating records through the object\'s create action: '
14179+
+ '`object-kanban` offers no quick-add control.',
1418314180
},
1418414181
// #19054 (ADR-0049 enforce-or-remove; maintainer ruling 2026-09-18) — the D3
1418514182
// entry of the `object-tenancy-organization-field-removed` family (ruling B on
@@ -23170,8 +23167,10 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly<Record<number, readonly string[]>>
2317023167
// #17260 — ADR-0049 enforce-or-remove, executing the objectui#8285
2317123168
// director-seat ruling (comment 5583979207, decision batch #91, 2026-09-08,
2317223169
// standing maintainer delegation): ruled option B — `quickAdd` is retired from
23173-
// the `object-kanban` board and stays only on the `kanban-ui` block, where a
23174-
// React host can supply the runtime function the control needs.
23170+
// the `object-kanban` board. The ruling kept the control on the `kanban-ui`
23171+
// block, where a React host can supply the runtime function it needs; objectui
23172+
// has since retired that block (objectui#8257), so `object-kanban` offers no
23173+
// quick-add control and no block a document can name offers one either.
2317523174
// The board FORWARDED the key but never honoured it: measured at the
2317623175
// `.objectui-sha` pin `53ded82bf`, `ObjectKanban.tsx:931` spreads the authored
2317723176
// bag into `KanbanRenderer` (`plugin-kanban/src/index.tsx:196` passes both
@@ -23187,7 +23186,7 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly<Record<number, readonly string[]>>
2318723186
// wrong. Tombstoned with `retiredKey()` in `ObjectKanbanPropsSchema` (the
2318823187
// surface baseline line carries `[RETIRED]`); sources are stripped by the D2
2318923188
// conversion `object-kanban-quick-add-removed`, a pure lossless delete scoped
23190-
// by component `type` so the LIVE `kanban-ui` spelling is untouched.
23189+
// by component `type` so the same key on any other component type is untouched.
2319123190
//
2319223191
// Registered under 18, not 17: v17.0.0 was cut before this landed, so the
2319323192
// removal ships on the 17.x line (launch-window convention: accept-set

‎packages/spec/src/ui/component.test.ts‎

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
} from './view.zod';
3333
import { FieldSchema } from '../data/field.zod';
3434
import { ALL_CONVERSIONS } from '../conversions/registry';
35+
import { MIGRATIONS_BY_MAJOR } from '../migrations/registry';
3536
import { strictObjectDeclarations } from '../shared/strict-object';
3637

3738
describe('PageHeaderProps', () => {
@@ -3578,8 +3579,9 @@ describe('ObjectKanbanPropsSchema limit — the row cap four objectui faces alre
35783579

35793580
// #17260 — the board's per-column quick-add switch, retired by the
35803581
// objectui#8285 director-seat ruling (comment 5583979207, decision batch #91,
3581-
// 2026-09-08; ruled option B: `quickAdd` leaves `object-kanban` and stays only
3582-
// on the React-host `kanban-ui` block). Unlike `limit` above — a key four
3582+
// 2026-09-08; ruled option B: `quickAdd` leaves `object-kanban`; the ruling
3583+
// named the React-host `kanban-ui` block as where it stays, and objectui has
3584+
// since retired that block, objectui#8257). Unlike `limit` above — a key four
35833585
// objectui faces already implemented, so the spec was the half that was wrong
35843586
// — `quickAdd` was FORWARDED and never read: at the pin this repo builds
35853587
// against (`.objectui-sha` = `e420df310`; re-measured there 2026-09-30 —
@@ -3627,9 +3629,45 @@ describe('ObjectKanbanPropsSchema quickAdd is retired (#17260)', () => {
36273629
it('rejects the retired `quickAdd` with the prescription, not a bare unknown-key verdict', () => {
36283630
// The prescription IS the payload: the author who hits this got
36293631
// `unknown-prop` from objectui's html tier before — the same message a
3630-
// typo gets — so the refusal has to say where the control still works.
3632+
// typo gets — so the refusal has to say what to do instead: delete the key.
36313633
expect(() => kanban.parse({ objectName: 'showcase_task', quickAdd: true }))
3632-
.toThrow(/`quickAdd`.*removed.*`kanban-ui`/s);
3634+
.toThrow(/`quickAdd`.*removed.*Delete the key/s);
3635+
});
3636+
3637+
it('prescribes no block objectui does not register — the remedy names no `kanban-ui`', () => {
3638+
// `kanban-ui` is a node type objectui retired (objectui#8257) and this spec
3639+
// never declared, so `PageComponentSchema` accepts it as an unregistered
3640+
// custom string: an author steered there writes a node that saves clean and
3641+
// resolves no renderer. The control itself has no metadata route, so the
3642+
// one sentence every copy shares is "delete the key" — pinned on the
3643+
// refusal, the D2 conversion's summary, the D3 entry's three texts and
3644+
// step 18's rationale (the paragraph the upgrade guide prints).
3645+
const refused = kanban.safeParse({ objectName: 'showcase_task', quickAdd: true });
3646+
expect(refused.success).toBe(false);
3647+
const issue = (refused.error?.issues ?? []).find((i) => i.path.join('.') === 'quickAdd');
3648+
expect(issue?.code).toBe('invalid_type'); // the refusal itself is unchanged
3649+
const tombstone = issue?.message ?? '';
3650+
expect(tombstone).toContain('Delete the key; `object-kanban` offers no quick-add control.');
3651+
3652+
const conversion = ALL_CONVERSIONS.find((c) => c.id === 'object-kanban-quick-add-removed');
3653+
const step18 = MIGRATIONS_BY_MAJOR[18];
3654+
const semantic = step18?.semantic.find((m) => m.id === 'object-kanban-quick-add-retired');
3655+
expect(conversion).toBeDefined();
3656+
expect(semantic).toBeDefined();
3657+
expect(step18?.rationale).toContain('retires `object-kanban`\'s `quickAdd`');
3658+
const remedyTexts = [
3659+
tombstone,
3660+
conversion?.summary ?? '',
3661+
semantic?.replacement ?? '',
3662+
semantic?.reason ?? '',
3663+
semantic?.acceptanceCriteria ?? '',
3664+
step18?.rationale ?? '',
3665+
];
3666+
for (const text of remedyTexts) {
3667+
expect(text.length).toBeGreaterThan(0);
3668+
expect(text).not.toContain('kanban-ui');
3669+
}
3670+
expect(semantic?.replacement).toContain('Delete the key; `object-kanban` offers no quick-add control.');
36333671
});
36343672

36353673
it('does not materialize the retired `quickAdd` on a clean parse', () => {

0 commit comments

Comments
 (0)