From 76265dfdc913da163e943872787191f43337c7fa Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 09:33:33 +0000 Subject: [PATCH 1/6] test(plugin-kanban): pin that KanbanEnhanced.tsx is deleted and nothing names it Red on the base: the file is present, and two test files still name it as a module specifier (a direct import and a vi.mock target). Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude --- .../kanbanEnhancedRetired-8932.test.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 packages/plugin-kanban/src/__tests__/kanbanEnhancedRetired-8932.test.ts diff --git a/packages/plugin-kanban/src/__tests__/kanbanEnhancedRetired-8932.test.ts b/packages/plugin-kanban/src/__tests__/kanbanEnhancedRetired-8932.test.ts new file mode 100644 index 0000000000..6b9d2027e2 --- /dev/null +++ b/packages/plugin-kanban/src/__tests__/kanbanEnhancedRetired-8932.test.ts @@ -0,0 +1,80 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#8932 — `KanbanEnhanced.tsx` is DELETED from `@object-ui/plugin-kanban`. + * + * The module was reachable through no entry of this package's `exports` map + * (`.` and `./style.css` only) and re-exported by no barrel, and once the + * `kanban-enhanced` registry key retired (objectui#8257) nothing but two test + * files referred to it. Ruled delete (2026-09-11, ratified by the maintainer + * 2026-09-24): published source that nothing can reach is removed, rather than + * given an export nobody asked for or left for the next reader to re-measure. + * ⛔ If a richer board is ever wanted, it comes back as a feature of the one + * registered board (`KanbanImpl`), not as a second implementation beside it. + * + * Two halves, each with a firing control on the same instrument: + * + * - the file is absent from `src/`, while `KanbanImpl.tsx` in the same + * directory reads present; + * - no file under `src/` names the retired module as a module specifier — a + * static or dynamic import, a re-export, a `require`, or a `vi.mock` / + * `vi.doMock` target — while the same pattern over the same walk finds the + * live board's lazy import in `index.tsx`. + * + * ⚠️ Mock targets count on purpose. A mock registered against a path that no + * longer exists fails nothing, so it would outlive the module silently; one of + * the two references this deletion found was exactly that shape. + */ + +import { describe, expect, it } from 'vitest'; +import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { dirname, join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** `packages/plugin-kanban/src` — one level up from `src/__tests__`. */ +const SRC_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); + +/** The deleted module, and the live board that is the control for it. */ +const RETIRED = 'KanbanEnhanced'; +const LIVE = 'KanbanImpl'; + +/** Every `.ts` / `.tsx` file under `src/`, recursively, as a path relative to `src/`. */ +function sourceFiles(dir: string = SRC_DIR): string[] { + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const full = join(dir, entry.name); + if (entry.isDirectory()) return sourceFiles(full); + return /\.tsx?$/.test(entry.name) ? [relative(SRC_DIR, full)] : []; + }); +} + +/** The files under `src/` that name `/MODULE` (extension optional) as a module specifier. */ +function filesReferencing(moduleName: string): string[] { + const specifier = new RegExp( + String.raw`(?:\bfrom|\bimport|\brequire|\bvi\.(?:do)?[mM]ock)\s*\(?\s*['"][^'"]*/${moduleName}(?:\.tsx?)?['"]`, + ); + return sourceFiles() + .filter((rel) => specifier.test(readFileSync(join(SRC_DIR, rel), 'utf8'))) + .sort(); +} + +describe('@object-ui/plugin-kanban — KanbanEnhanced.tsx is deleted (objectui#8932)', () => { + it('the module file is gone from src/, while the live board beside it reads present', () => { + expect({ + retired: existsSync(join(SRC_DIR, `${RETIRED}.tsx`)), + live: existsSync(join(SRC_DIR, `${LIVE}.tsx`)), + }).toEqual({ retired: false, live: true }); + }); + + it('no file under src/ names the deleted module as a specifier, while the same pattern finds the live board', () => { + // Firing control first: the walk and the pattern are live, so the empty + // list below is a reading and not a probe that matches nothing. + expect(filesReferencing(LIVE)).toContain('index.tsx'); + expect(filesReferencing(RETIRED)).toEqual([]); + }); +}); From 2165ee472abc7f42f193889bb17f49f832c6cefd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 09:35:55 +0000 Subject: [PATCH 2/6] fix(plugin-kanban): delete KanbanEnhanced.tsx, the module no export or barrel reached Executes the objectui#8932 ruling (delete). The package `exports` map is `.` and `./style.css` only and the entry point never re-exported the component, so nothing supported moves; the published package loses the `dist/KanbanEnhanced.d.ts` typings, the only part of the module it shipped. - cardPredicateScope.test.tsx: the KanbanEnhanced positive leg goes (the KanbanBoard leg pins the same verdict); its fail-soft and bare-field legs now run on KanbanBoard. - kanban-handler-slots-7664.test.tsx: the unread KanbanEnhanced prop recorder goes; no assertion moves. - index.tsx: the kanban-enhanced tombstone no longer says the file stays. - one-authority collision baseline: ConditionalFormattingRule loses the deleted third site (shrink direction). - two pending changesets scoped to "at this change" with a dated note. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude --- .../6172-markdown-kanban-one-authority.md | 7 +- ...257-8008-kanban-gantt-family-retirement.md | 15 +- .changeset/8932-retire-kanban-enhanced.md | 28 + packages/plugin-kanban/src/KanbanEnhanced.tsx | 499 ------------------ .../kanban-handler-slots-7664.test.tsx | 25 +- .../src/cardPredicateScope.test.tsx | 23 +- packages/plugin-kanban/src/index.tsx | 30 +- ...e-authority-per-exported-name-6273.test.ts | 4 +- 8 files changed, 87 insertions(+), 544 deletions(-) create mode 100644 .changeset/8932-retire-kanban-enhanced.md delete mode 100644 packages/plugin-kanban/src/KanbanEnhanced.tsx diff --git a/.changeset/6172-markdown-kanban-one-authority.md b/.changeset/6172-markdown-kanban-one-authority.md index cbb6e6a6dd..8c8daafe52 100644 --- a/.changeset/6172-markdown-kanban-one-authority.md +++ b/.changeset/6172-markdown-kanban-one-authority.md @@ -36,7 +36,12 @@ extended, so it was always inherited rather than added by the plugin. one.** `KanbanImpl.tsx` and `KanbanEnhanced.tsx` each redeclared both names. A TypeScript-AST comparison found them strict-SUBSET copies of `./types` with nothing typed differently, so their extra members moved onto the one -declaration and both files now re-point at it. +declaration and, at this change, both files re-pointed at it. + +⚠️ **Dated note, 2026-09-25 — `KanbanEnhanced.tsx` has since been deleted — +objectui#8932.** Later in this same release that module left the package, so +`KanbanImpl.tsx` is the one file of the two that remains. The convergence above +is unaffected; the objectui#8932 entry states what ships. Additive for consumers: `KanbanCard` gains `cardSubtitle`, `cardFieldCells` and `coverImage`; `KanbanColumn` gains `collapsed`. All four are optional, so every diff --git a/.changeset/8802-8257-8008-kanban-gantt-family-retirement.md b/.changeset/8802-8257-8008-kanban-gantt-family-retirement.md index 51518cd0e2..7555488cf7 100644 --- a/.changeset/8802-8257-8008-kanban-gantt-family-retirement.md +++ b/.changeset/8802-8257-8008-kanban-gantt-family-retirement.md @@ -138,10 +138,17 @@ is a different case, and the earlier draft of this note stated it wrongly: this package's `exports` map has exactly two entries — `.` and `./style.css` — and the barrel never re-exported the component, so `@object-ui/plugin-kanban/KanbanEnhanced` has never been a resolvable specifier -for a consumer. With `kanban-enhanced` unregistered, `KanbanEnhanced.tsx` has -zero non-test importers. ⛔ The file is deliberately left in place: deleting -published-but-unreachable source is a further narrowing and needs its own -maintainer ruling, which this change does not have. +for a consumer. At this change, with `kanban-enhanced` unregistered, +`KanbanEnhanced.tsx` had zero non-test importers, and the file was deliberately +left in place: deleting published-but-unreachable source is a further narrowing +and needed its own maintainer ruling, which this change did not have. + +⚠️ **Dated note, 2026-09-25 — that ruling came, and the file has since been +deleted — objectui#8932.** Later in this same release +`packages/plugin-kanban/src/KanbanEnhanced.tsx` was deleted, and with it the +`dist/KanbanEnhanced.d.ts` typings this package shipped for it. The rest of this +entry is kept as the reading of this change; the objectui#8932 entry states what +ships. **⚠️ `@object-ui/sdui-parser`: `QUICK_ADD_HOST_TYPES` loses `kanban` with the registration.** The `inert-quick-add` diagnostic (objectui#8285) named the two diff --git a/.changeset/8932-retire-kanban-enhanced.md b/.changeset/8932-retire-kanban-enhanced.md new file mode 100644 index 0000000000..1bb739b156 --- /dev/null +++ b/.changeset/8932-retire-kanban-enhanced.md @@ -0,0 +1,28 @@ +--- +'@object-ui/plugin-kanban': patch +--- + +Delete `packages/plugin-kanban/src/KanbanEnhanced.tsx` (objectui#8932, ruling of +2026-09-11, ratified by the maintainer 2026-09-24). + +**What leaves the published package:** `dist/KanbanEnhanced.d.ts` and +`dist/KanbanEnhanced.d.ts.map`, the typings emitted for that module. They were +the only part of it this package ever shipped. The `KanbanEnhanced` component +itself was never in `dist/index.js` or `dist/index.umd.cjs`, because nothing the +entry point imports reached it. + +**No supported import changes.** This package's `exports` map has exactly two +entries, `.` and `./style.css`, and the entry point never re-exported +`KanbanEnhanced`, so `@object-ui/plugin-kanban/KanbanEnhanced` was never a +resolvable specifier. Its registry key, `kanban-enhanced`, retired earlier in +this release (objectui#8257). After that, only two test files referred to the +module. + +**If you deep-imported it** through a path into +`node_modules/@object-ui/plugin-kanban/dist/` that the `exports` map never +allowed, that path only ever reached typings with no JavaScript behind them, and +now it reaches nothing. Render the one registered board, `object-kanban`, instead. +`KanbanCard` and `KanbanColumn`, which the module re-exported, are still +exported from `@object-ui/plugin-kanban`. The card-formatting rule type it +aliased is `KanbanConditionalFormattingRule` in `@object-ui/types`. +`KanbanEnhancedProps` is gone along with the component. diff --git a/packages/plugin-kanban/src/KanbanEnhanced.tsx b/packages/plugin-kanban/src/KanbanEnhanced.tsx deleted file mode 100644 index c582011172..0000000000 --- a/packages/plugin-kanban/src/KanbanEnhanced.tsx +++ /dev/null @@ -1,499 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import * as React from "react" -import { useVirtualizer } from "@tanstack/react-virtual" -import { - DndContext, - DragEndEvent, - DragOverlay, - DragStartEvent, - PointerSensor, - useSensor, - useSensors, - closestCorners, -} from "@dnd-kit/core" -import { - SortableContext, - arrayMove, - useSortable, - verticalListSortingStrategy, -} from "@dnd-kit/sortable" -import { CSS } from "@dnd-kit/utilities" -import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Button, Input } from "@object-ui/components" -import { resolveConditionalFormatting } from "@object-ui/core" -import { usePredicateScope } from "@object-ui/react" -import type { KanbanConditionalFormattingRule } from "@object-ui/types" -import type { KanbanCard, KanbanColumn } from './types' -import { ChevronDown, ChevronRight, AlertTriangle, Plus } from "lucide-react" - -const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ') - -// One authority for these two names in this package: `./types` (objectui#6172 / #6155). -// This file's former local copies added `coverImage` (card) and `collapsed` -// (column); both now live on the canonical declaration as optional members, so -// this module sees exactly the shape it declared before. The re-export -// preserves the export surface; it is not a second declaration. -export type { KanbanCard, KanbanColumn } from './types' - -// Card formatting accepts the native `{ field, operator, value }` shape and the -// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types. -export type ConditionalFormattingRule = KanbanConditionalFormattingRule - -export interface KanbanEnhancedProps { - columns: KanbanColumn[] - onCardMove?: (cardId: string, fromColumnId: string, toColumnId: string, newIndex: number) => void - onColumnToggle?: (columnId: string, collapsed: boolean) => void - enableVirtualScrolling?: boolean - virtualScrollThreshold?: number - className?: string - quickAdd?: boolean - onQuickAdd?: (columnId: string, title: string) => void - conditionalFormatting?: ConditionalFormattingRule[] -} - -// Card conditional formatting now delegates to the shared CEL evaluator -// (issue #1584 / ADR-0058) so kanban cards, list rows, and grid rows reach the -// identical verdict. Beyond the native `{ field, operator, value }` rules the -// kanban schema declares, this also accepts spec `{ condition, style }` rules. -// The host predicate scope is bound alongside the card so `features.*` / -// `current_user.*` conditions resolve here exactly as they do on grid rows. -function getCardStyles( - card: KanbanCard, - rules?: ConditionalFormattingRule[], - scope?: Record, -): React.CSSProperties { - return resolveConditionalFormatting(card as Record, rules as any, scope) as React.CSSProperties -} - -function SortableCard({ card, conditionalFormatting }: { card: KanbanCard; conditionalFormatting?: ConditionalFormattingRule[] }) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: card.id }) - - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : undefined, - } - - const predicateScope = usePredicateScope() - const cardStyles = getCardStyles(card, conditionalFormatting, predicateScope) - - return ( -
- - {card.coverImage && ( -
- -
- )} - - {card.title} - {card.description && ( - - {card.description} - - )} - - {card.badges && card.badges.length > 0 && ( - -
- {card.badges.map((badge, index) => ( - - {badge.label} - - ))} -
-
- )} -
-
- ) -} - -function VirtualizedCardList({ cards, parentRef, conditionalFormatting }: { cards: KanbanCard[]; parentRef: React.RefObject; conditionalFormatting?: ConditionalFormattingRule[] }) { - const rowVirtualizer = useVirtualizer({ - count: cards.length, - getScrollElement: () => parentRef.current, - estimateSize: () => 120, - overscan: 5, - }) - - return ( -
- {rowVirtualizer.getVirtualItems().map((virtualItem) => { - const card = cards[virtualItem.index] - return ( -
- -
- ) - })} -
- ) -} - -function QuickAddForm({ columnId, onAdd }: { columnId: string; onAdd: (columnId: string, title: string) => void }) { - const [isAdding, setIsAdding] = React.useState(false) - const [title, setTitle] = React.useState('') - const inputRef = React.useRef(null) - - const handleSubmit = () => { - const trimmed = title.trim() - if (trimmed) { - onAdd(columnId, trimmed) - setTitle('') - } - setIsAdding(false) - } - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault() - handleSubmit() - } else if (e.key === 'Escape') { - setTitle('') - setIsAdding(false) - } - } - - if (!isAdding) { - return ( - - ) - } - - return ( -
- setTitle(e.target.value)} - onKeyDown={handleKeyDown} - onBlur={handleSubmit} - placeholder="Enter card title..." - className="text-sm" - autoFocus - /> -
- ) -} - -function KanbanColumnEnhanced({ - column, - cards, - onToggle, - enableVirtual, - quickAdd, - onQuickAdd, - conditionalFormatting, -}: { - column: KanbanColumn - cards: KanbanCard[] - onToggle: (collapsed: boolean) => void - enableVirtual: boolean - quickAdd?: boolean - onQuickAdd?: (columnId: string, title: string) => void - conditionalFormatting?: ConditionalFormattingRule[] -}) { - const safeCards = cards || [] - const scrollRef = React.useRef(null) - const { setNodeRef } = useSortable({ - id: column.id, - data: { - type: "column", - }, - }) - - const isLimitExceeded = column.limit && safeCards.length >= column.limit - const isNearLimit = column.limit && safeCards.length >= column.limit * 0.8 - - return ( -
-
-
- - {!column.collapsed && ( - <> -

- {column.title} -

-
- - {safeCards.length} - {column.limit && ` / ${column.limit}`} - - {isLimitExceeded && ( - - Full - - )} - {isNearLimit && !isLimitExceeded && ( - - )} -
- - )} -
- {column.collapsed && ( -
- - {column.title} - - - {safeCards.length} - -
- )} -
- {!column.collapsed && ( -
- c.id)} - strategy={verticalListSortingStrategy} - > - {enableVirtual ? ( - - ) : ( -
- {safeCards.map((card) => ( - - ))} -
- )} -
- {quickAdd && onQuickAdd && ( - - )} -
- )} -
- ) -} - -export function KanbanEnhanced({ - columns, - onCardMove, - onColumnToggle, - enableVirtualScrolling = false, - virtualScrollThreshold = 50, - className, - quickAdd, - onQuickAdd, - conditionalFormatting, -}: KanbanEnhancedProps) { - const [activeCard, setActiveCard] = React.useState(null) - - const safeColumns = React.useMemo(() => { - return (columns || []).map(col => ({ - ...col, - cards: col.cards || [] - })); - }, [columns]); - - const [boardColumns, setBoardColumns] = React.useState(safeColumns) - - React.useEffect(() => { - setBoardColumns(safeColumns) - }, [safeColumns]) - - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 8, - }, - }) - ) - - const handleDragStart = (event: DragStartEvent) => { - const { active } = event - const card = findCard(active.id as string) - setActiveCard(card) - } - - const handleDragEnd = (event: DragEndEvent) => { - const { active, over } = event - setActiveCard(null) - - if (!over) return - - const activeId = active.id as string - const overId = over.id as string - - if (activeId === overId) return - - const activeColumn = findColumnByCardId(activeId) - const overColumn = findColumnByCardId(overId) || findColumnById(overId) - - if (!activeColumn || !overColumn) return - - if (activeColumn.id === overColumn.id) { - const cards = [...activeColumn.cards] - const oldIndex = cards.findIndex((c) => c.id === activeId) - const newIndex = cards.findIndex((c) => c.id === overId) - - const newCards = arrayMove(cards, oldIndex, newIndex) - setBoardColumns((prev) => - prev.map((col) => - col.id === activeColumn.id ? { ...col, cards: newCards } : col - ) - ) - } else { - const activeCards = [...activeColumn.cards] - const overCards = [...overColumn.cards] - const activeIndex = activeCards.findIndex((c) => c.id === activeId) - - const isDroppingOnColumn = overId === overColumn.id - const overIndex = isDroppingOnColumn - ? overCards.length - : overCards.findIndex((c) => c.id === overId) - - const [movedCard] = activeCards.splice(activeIndex, 1) - overCards.splice(overIndex, 0, movedCard) - - setBoardColumns((prev) => - prev.map((col) => { - if (col.id === activeColumn.id) { - return { ...col, cards: activeCards } - } - if (col.id === overColumn.id) { - return { ...col, cards: overCards } - } - return col - }) - ) - - if (onCardMove) { - onCardMove(activeId, activeColumn.id, overColumn.id, overIndex) - } - } - } - - const findCard = React.useCallback( - (cardId: string): KanbanCard | null => { - for (const column of boardColumns) { - const card = column.cards.find((c) => c.id === cardId) - if (card) return card - } - return null - }, - [boardColumns] - ) - - const findColumnByCardId = React.useCallback( - (cardId: string): KanbanColumn | null => { - return boardColumns.find((col) => col.cards.some((c) => c.id === cardId)) || null - }, - [boardColumns] - ) - - const findColumnById = React.useCallback( - (columnId: string): KanbanColumn | null => { - return boardColumns.find((col) => col.id === columnId) || null - }, - [boardColumns] - ) - - const handleColumnToggle = React.useCallback((columnId: string, collapsed: boolean) => { - setBoardColumns(prev => - prev.map(col => col.id === columnId ? { ...col, collapsed } : col) - ) - onColumnToggle?.(columnId, collapsed) - }, [onColumnToggle]) - - return ( - -
- {boardColumns.map((column) => { - const shouldUseVirtual = enableVirtualScrolling && column.cards.length > virtualScrollThreshold - return ( - handleColumnToggle(column.id, collapsed)} - enableVirtual={shouldUseVirtual} - quickAdd={quickAdd} - onQuickAdd={onQuickAdd} - conditionalFormatting={conditionalFormatting} - /> - ) - })} -
- - {activeCard ? : null} - -
- ) -} - -export default KanbanEnhanced; diff --git a/packages/plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx b/packages/plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx index fd608943ad..479da75953 100644 --- a/packages/plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx +++ b/packages/plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx @@ -59,9 +59,15 @@ * the DERIVATION from the read site, which is the thing a re-key cannot hold * constant. * + * ## What objectui#8932 removed from this file + * + * The prop recorder for the `KanbanEnhanced` chunk, deleted with that module. + * No leg read it once `kanban-enhanced` retired: every probe below records the + * `KanbanImpl` chunk, so no assertion moved or went. + * * ## Suite 1 — runtime reachability, per registration * - * The two lazy board chunks are replaced by prop recorders; three spies are + * The lazy board chunk is replaced by a prop recorder; three spies are * authored on the schema; the question is which of them reach the board. * * - `KanbanRenderer` rendered directly (`../index.tsx`): all three arrive BY @@ -128,26 +134,19 @@ import { ObjectKanbanSchema as ObjectKanbanZod } from '@object-ui/types/zod'; import { KanbanRenderer } from '../index'; import '../index'; -/** Every props object either board implementation was rendered with, in order. */ +/** Every props object the board implementation was rendered with, in order. */ const recorded = vi.hoisted(() => ({ impl: [] as Array>, - enhanced: [] as Array>, })); -// Both lazy chunks are replaced by prop recorders: the question this file asks -// is what reaches the board's props, not what the board draws with them. +// The lazy board chunk is replaced by a prop recorder: the question this file +// asks is what reaches the board's props, not what the board draws with them. vi.mock('../KanbanImpl', () => ({ default: (props: Record) => { recorded.impl.push(props); return null; }, })); -vi.mock('../KanbanEnhanced', () => ({ - default: (props: Record) => { - recorded.enhanced.push(props); - return null; - }, -})); const STATIC_COLUMNS = [{ id: 'todo', title: 'To Do', cards: [{ id: '1', title: 'One' }] }]; @@ -156,7 +155,7 @@ function authored() { } /** Wait for one more board render to be recorded, then return the props it got. */ -async function lastBoardProps(log: 'impl' | 'enhanced', before: number, unmount: () => void) { +async function lastBoardProps(log: keyof typeof recorded, before: number, unmount: () => void) { await waitFor(() => expect(recorded[log].length).toBeGreaterThan(before)); const received = recorded[log][recorded[log].length - 1]; unmount(); @@ -171,7 +170,7 @@ async function lastBoardProps(log: 'impl' | 'enhanced', before: number, unmount: * lanes statically and nothing here fetches. (The prop is required and typed * `any`, so the value has to be spelled rather than omitted.) */ -async function boardPropsFor(type: string, log: 'impl' | 'enhanced', schemaKeys: Record) { +async function boardPropsFor(type: string, log: keyof typeof recorded, schemaKeys: Record) { const Renderer = ComponentRegistry.get(type) as React.ComponentType>; expect(Renderer, `\`${type}\` is not registered`).toBeDefined(); const before = recorded[log].length; diff --git a/packages/plugin-kanban/src/cardPredicateScope.test.tsx b/packages/plugin-kanban/src/cardPredicateScope.test.tsx index 5e311a603f..bab0fcfa08 100644 --- a/packages/plugin-kanban/src/cardPredicateScope.test.tsx +++ b/packages/plugin-kanban/src/cardPredicateScope.test.tsx @@ -13,13 +13,19 @@ * identical verdict on grid rows and kanban cards. Before this suite, kanban * evaluated cards without the ambient scope, so such a condition silently * never matched here while working on the grid. + * + * objectui#8932 deleted `KanbanEnhanced`, the second board this suite used to + * drive. Its positive leg went with it — the `KanbanBoard` leg pins the same + * verdict on the one registered board. The two legs that only ever ran on it + * now run on `KanbanBoard`: the fail-soft reading outside the provider, which + * is the negative control that makes the live positive leg a reading, and the + * objectui#5741 bare-field canon. */ import { describe, it, expect, afterEach } from 'vitest'; import * as React from 'react'; import { render, cleanup } from '@testing-library/react'; import { PredicateScopeProvider } from '@object-ui/react'; -import { KanbanEnhanced } from './KanbanEnhanced'; import KanbanBoard from './KanbanImpl'; // happy-dom may lack ResizeObserver (KanbanBoard's container-aware column @@ -51,15 +57,6 @@ function findByBg(container: HTMLElement, bg: string): HTMLElement | undefined { afterEach(cleanup); describe('kanban card conditional formatting · host predicate scope (ADR-0058)', () => { - it('KanbanEnhanced binds the ambient predicate scope alongside the card', () => { - const { container } = render( - - - , - ); - expect(findByBg(container, HOT_BG)).toBeTruthy(); - }); - it('KanbanBoard (impl) binds the ambient predicate scope alongside the card', () => { const { container } = render( @@ -71,7 +68,7 @@ describe('kanban card conditional formatting · host predicate scope (ADR-0058)' it('a scope-gated condition fails SOFT (no style) outside the provider', () => { const { container } = render( - , + , ); expect(findByBg(container, HOT_BG)).toBeUndefined(); }); @@ -82,11 +79,11 @@ describe('kanban card conditional formatting · host predicate scope (ADR-0058)' // to "no style". The canonical spelling on the same card is the control // that proves the rule was consulted at all. const bareRules = [{ condition: 'id == "c1"', style: { backgroundColor: HOT_BG } }] as any; - const bare = render(); + const bare = render(); expect(findByBg(bare.container, HOT_BG)).toBeUndefined(); cleanup(); const canonRules = [{ condition: 'record.id == "c1"', style: { backgroundColor: HOT_BG } }] as any; - const canon = render(); + const canon = render(); expect(findByBg(canon.container, HOT_BG)).toBeTruthy(); }); }); diff --git a/packages/plugin-kanban/src/index.tsx b/packages/plugin-kanban/src/index.tsx index 7e09f2d928..33c09e7dda 100644 --- a/packages/plugin-kanban/src/index.tsx +++ b/packages/plugin-kanban/src/index.tsx @@ -452,20 +452,24 @@ export const kanbanComponents = { * the same firing control (`object-grid`, 2 JSON / 128 TS) and silent control * (`zzz-not-a-type`, 0) the `kanban-ui` note above cites. * - * `KanbanEnhanced.tsx` itself is untouched on disk. ⛔ It is NOT, and never - * was, reachable from outside this package: `package.json` `exports` publishes - * exactly two entries — `.` and `./style.css` — and this barrel does not - * re-export the component, so `@object-ui/plugin-kanban/KanbanEnhanced` has - * never been a resolvable specifier for a consumer. (An earlier revision of - * this note claimed it was; that claim was wrong and is corrected here rather - * than deleted, because it is what a reader would otherwise copy.) What this - * card removes is the registry key and the `React.lazy` wrapper that existed - * only to serve it; what it leaves behind is a module with zero non-test - * importers — `cardPredicateScope.test.tsx` reaches it by relative path. - * ⛔ Deleting the file is a FURTHER narrowing of published source and needs its - * own maintainer ruling, which this card does not carry, so it stays. + * `KanbanEnhanced` was NOT, and never had been, reachable from outside this + * package: `package.json` `exports` publishes exactly two entries — `.` and + * `./style.css` — and this barrel never re-exported the component, so + * `@object-ui/plugin-kanban/KanbanEnhanced` was never a resolvable specifier + * for a consumer. (An earlier revision of this note claimed it was; that claim + * was wrong and is corrected here rather than deleted, because it is what a + * reader would otherwise copy.) What this card removed is the registry key and + * the `React.lazy` wrapper that existed only to serve it. It left the module + * itself on disk, with zero non-test importers, because deleting published + * source is a further narrowing that needed its own maintainer ruling. + * + * ⚠️ That ruling came: objectui#8932 (2026-09-11, ratified 2026-09-24) deleted + * `KanbanEnhanced.tsx`, the two test references it had left, and with them the + * `dist/KanbanEnhanced.d.ts` typings the package used to ship — the only part + * of the module that was ever published, since the bundle never contained it. * - * Pinned in `src/__tests__/kanban-family-registry-keys-retired-8257.test.ts`. + * Pinned in `src/__tests__/kanban-family-registry-keys-retired-8257.test.ts` + * (the key) and `src/__tests__/kanbanEnhancedRetired-8932.test.ts` (the file). */ /** diff --git a/scripts/__tests__/one-authority-per-exported-name-6273.test.ts b/scripts/__tests__/one-authority-per-exported-name-6273.test.ts index ef937e811a..0a8b7933d9 100644 --- a/scripts/__tests__/one-authority-per-exported-name-6273.test.ts +++ b/scripts/__tests__/one-authority-per-exported-name-6273.test.ts @@ -394,7 +394,9 @@ const KNOWN_COLLISIONS: ReadonlyMap = new Map([ // a derived declaration is still an authority. That contrast is why #6298 // took the re-export route rather than the derive route. ['ComponentMeta', ['packages/core/src/registry/Registry.ts', 'packages/types/src/base.ts']], - ['ConditionalFormattingRule', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/types/src/objectql.ts']], + // `ConditionalFormattingRule` had a THIRD site, `packages/plugin-kanban/src/KanbanEnhanced.tsx`, + // which left the tree when objectui#8932 deleted that module; the two below still collide. + ['ConditionalFormattingRule', ['packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/types/src/objectql.ts']], ['ConfirmDialogState', ['packages/app-shell/src/views/ActionConfirmDialog.tsx', 'packages/plugin-designer/src/hooks/useConfirmDialog.ts']], ['ConnectionState', ['packages/collaboration/src/useRealtimeSubscription.ts', 'packages/data-objectstack/src/index.ts']], ['DataSource', ['packages/app-shell/src/types.ts', 'packages/types/src/data.ts']], From 5a9a73d6e19ed6cc0c2c22273c2e0c4c7645ddfb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 10:04:50 +0000 Subject: [PATCH 3/6] fix(plugin-kanban): drop @tanstack/react-virtual, whose only consumer was KanbanEnhanced.tsx The deleted module was the one file in @object-ui/plugin-kanban that imported it, so the declaration had no consumer left and check-unused-dependencies went red on the previous commit. The lockfile loses only this importer's three lines; @object-ui/plugin-grid still declares the package. The objectui#8932 changeset says so for anyone who got it transitively. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude --- .changeset/8932-retire-kanban-enhanced.md | 5 +++++ packages/plugin-kanban/package.json | 1 - pnpm-lock.yaml | 3 --- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.changeset/8932-retire-kanban-enhanced.md b/.changeset/8932-retire-kanban-enhanced.md index 1bb739b156..543c9e6dab 100644 --- a/.changeset/8932-retire-kanban-enhanced.md +++ b/.changeset/8932-retire-kanban-enhanced.md @@ -26,3 +26,8 @@ now it reaches nothing. Render the one registered board, `object-kanban`, instea exported from `@object-ui/plugin-kanban`. The card-formatting rule type it aliased is `KanbanConditionalFormattingRule` in `@object-ui/types`. `KanbanEnhancedProps` is gone along with the component. + +**One dependency leaves with it.** `@tanstack/react-virtual` is no longer in this +package's `dependencies`: the deleted module was the only file in the package +that imported it. If your code imports `@tanstack/react-virtual` and got it only +through `@object-ui/plugin-kanban`, declare it in your own `package.json`. diff --git a/packages/plugin-kanban/package.json b/packages/plugin-kanban/package.json index f22f7bff86..d36da81807 100644 --- a/packages/plugin-kanban/package.json +++ b/packages/plugin-kanban/package.json @@ -43,7 +43,6 @@ "@object-ui/plugin-detail": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@tanstack/react-virtual": "^3.14.9", "lucide-react": "^1.31.0" }, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d105086a6..2a0cccf07d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2204,9 +2204,6 @@ importers: '@object-ui/types': specifier: workspace:* version: link:../types - '@tanstack/react-virtual': - specifier: ^3.14.9 - version: 3.14.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) lucide-react: specifier: ^1.31.0 version: 1.35.0(react@19.2.8) From a843148be68dd2799ad1d11779462914d321f1ff Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 10:09:52 +0000 Subject: [PATCH 4/6] docs(changeset): name the three style.css rules the deletion drops Measured by rebuilding dist/index.css with and without the module: 65 rules kept becomes 62, and the three that leave are border-border/50, text-yellow-500 and shadow-xl. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude --- .changeset/8932-retire-kanban-enhanced.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.changeset/8932-retire-kanban-enhanced.md b/.changeset/8932-retire-kanban-enhanced.md index 543c9e6dab..b2b2e6141a 100644 --- a/.changeset/8932-retire-kanban-enhanced.md +++ b/.changeset/8932-retire-kanban-enhanced.md @@ -11,6 +11,11 @@ the only part of it this package ever shipped. The `KanbanEnhanced` component itself was never in `dist/index.js` or `dist/index.umd.cjs`, because nothing the entry point imports reached it. +`@object-ui/plugin-kanban/style.css` also loses three utility rules that no other +file in this package uses: `border-border/50`, `text-yellow-500` and `shadow-xl`. +If your own markup relied on this sheet for any of the three, generate them in +your own Tailwind build. + **No supported import changes.** This package's `exports` map has exactly two entries, `.` and `./style.css`, and the entry point never re-exported `KanbanEnhanced`, so `@object-ui/plugin-kanban/KanbanEnhanced` was never a From abda7fc09e03bb7364c54e3a23ff76d56c11ace7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 10:42:06 +0000 Subject: [PATCH 5/6] fix(plugin-kanban,types): true changeset prose, and types JSDoc that no longer names KanbanEnhanced as a reader Contract review of a843148be (PR comment 5831016914): - The changeset said the component was never in dist/index.js or the UMD bundle. Published 17.6.0 inlines it behind the kanban-enhanced key and exports `.` only. The entry now says the component left the bundle with the objectui#8257 / objectui#8802 key retirement, lists what this change removes (the source file, dist/KanbanEnhanced.d.ts(.map), the orphaned react-virtual dependency), and says style.css first ships in this release without the three rules. The index.tsx tombstone carried the same false clause and is corrected the same way. - Four present-tense JSDoc passages in @object-ui/types (KanbanCard, KanbanColumn.cards, ObjectKanbanSchema.columns twice) named KanbanEnhanced as a live reader; KanbanImpl is now the one board, and the past counts are dated. Comment-only, with an @object-ui/types patch changeset. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude --- .../8932-kanban-enhanced-types-jsdoc.md | 5 ++ .changeset/8932-retire-kanban-enhanced.md | 64 ++++++++++--------- packages/plugin-kanban/src/index.tsx | 5 +- packages/types/src/complex.ts | 10 +-- packages/types/src/objectql.ts | 12 ++-- 5 files changed, 56 insertions(+), 40 deletions(-) create mode 100644 .changeset/8932-kanban-enhanced-types-jsdoc.md diff --git a/.changeset/8932-kanban-enhanced-types-jsdoc.md b/.changeset/8932-kanban-enhanced-types-jsdoc.md new file mode 100644 index 0000000000..b6ab17815e --- /dev/null +++ b/.changeset/8932-kanban-enhanced-types-jsdoc.md @@ -0,0 +1,5 @@ +--- +'@object-ui/types': patch +--- + +The JSDoc on `KanbanCard`, `KanbanColumn.cards` and `ObjectKanbanSchema.columns` no longer names `KanbanEnhanced` as a board that reads these members. `KanbanImpl` has been the one kanban board since objectui#8932 deleted that module, and the older counts that included it now carry a date. Only comments change; no type moves. diff --git a/.changeset/8932-retire-kanban-enhanced.md b/.changeset/8932-retire-kanban-enhanced.md index b2b2e6141a..5ea1a40b2a 100644 --- a/.changeset/8932-retire-kanban-enhanced.md +++ b/.changeset/8932-retire-kanban-enhanced.md @@ -5,34 +5,40 @@ Delete `packages/plugin-kanban/src/KanbanEnhanced.tsx` (objectui#8932, ruling of 2026-09-11, ratified by the maintainer 2026-09-24). -**What leaves the published package:** `dist/KanbanEnhanced.d.ts` and -`dist/KanbanEnhanced.d.ts.map`, the typings emitted for that module. They were -the only part of it this package ever shipped. The `KanbanEnhanced` component -itself was never in `dist/index.js` or `dist/index.umd.cjs`, because nothing the -entry point imports reached it. - -`@object-ui/plugin-kanban/style.css` also loses three utility rules that no other -file in this package uses: `border-border/50`, `text-yellow-500` and `shadow-xl`. -If your own markup relied on this sheet for any of the three, generate them in -your own Tailwind build. - -**No supported import changes.** This package's `exports` map has exactly two -entries, `.` and `./style.css`, and the entry point never re-exported -`KanbanEnhanced`, so `@object-ui/plugin-kanban/KanbanEnhanced` was never a -resolvable specifier. Its registry key, `kanban-enhanced`, retired earlier in -this release (objectui#8257). After that, only two test files referred to the -module. +**How the component left the bundle.** Through 17.6.0 the `KanbanEnhanced` +component shipped inlined in `dist/index.js` and `dist/index.umd.cjs`, reachable +through the `kanban-enhanced` registry key and the `kanbanComponents` map. It +leaves the bundle in this release together with that key (objectui#8257 / +objectui#8802, the kanban and gantt family retirement entry). After that +retirement, only two test files still referred to the source file. + +**What this change removes:** + +- the source file; +- `dist/KanbanEnhanced.d.ts` and `dist/KanbanEnhanced.d.ts.map`, the typings the + build still emitted for it; +- the `@tanstack/react-virtual` dependency, whose only consumer in this package + was that file. + +**No supported import changes.** Neither the 17.6.0 `exports` map (`.` only) nor +this release's (`.` and `./style.css`) has a `./KanbanEnhanced` entry. Neither +entry point exports the component by name. `@object-ui/plugin-kanban/KanbanEnhanced` +is not a resolvable specifier. **If you deep-imported it** through a path into -`node_modules/@object-ui/plugin-kanban/dist/` that the `exports` map never -allowed, that path only ever reached typings with no JavaScript behind them, and -now it reaches nothing. Render the one registered board, `object-kanban`, instead. -`KanbanCard` and `KanbanColumn`, which the module re-exported, are still -exported from `@object-ui/plugin-kanban`. The card-formatting rule type it -aliased is `KanbanConditionalFormattingRule` in `@object-ui/types`. -`KanbanEnhancedProps` is gone along with the component. - -**One dependency leaves with it.** `@tanstack/react-virtual` is no longer in this -package's `dependencies`: the deleted module was the only file in the package -that imported it. If your code imports `@tanstack/react-virtual` and got it only -through `@object-ui/plugin-kanban`, declare it in your own `package.json`. +`node_modules/@object-ui/plugin-kanban/dist/` that the `exports` map does not +allow, that path reached `dist/KanbanEnhanced.d.ts` in 17.6.0. Those typings had +no `dist/KanbanEnhanced.js` beside them, and now the path reaches nothing. Render +the one registered board, `object-kanban`, instead. `KanbanCard` and +`KanbanColumn`, which the module re-exported, are still exported from +`@object-ui/plugin-kanban`. The card-formatting rule type it aliased is +`KanbanConditionalFormattingRule` in `@object-ui/types`. `KanbanEnhancedProps` is +gone along with the component. + +**If you import `@tanstack/react-virtual` yourself** and got it only through +`@object-ui/plugin-kanban`, declare it in your own `package.json`. + +**`style.css`.** This package's stylesheet, `@object-ui/plugin-kanban/style.css`, +first ships in this release (objectui#4929). It is built after this deletion, so +it does not carry the three utility rules only that module used: +`border-border/50`, `text-yellow-500` and `shadow-xl`. diff --git a/packages/plugin-kanban/src/index.tsx b/packages/plugin-kanban/src/index.tsx index 33c09e7dda..42af46a957 100644 --- a/packages/plugin-kanban/src/index.tsx +++ b/packages/plugin-kanban/src/index.tsx @@ -465,8 +465,9 @@ export const kanbanComponents = { * * ⚠️ That ruling came: objectui#8932 (2026-09-11, ratified 2026-09-24) deleted * `KanbanEnhanced.tsx`, the two test references it had left, and with them the - * `dist/KanbanEnhanced.d.ts` typings the package used to ship — the only part - * of the module that was ever published, since the bundle never contained it. + * `dist/KanbanEnhanced.d.ts` typings the package still emitted for it. (Through + * 17.6.0 the component itself was also bundled into `dist/index.js`, behind the + * `kanban-enhanced` key; it left the bundle with this card's retirement.) * * Pinned in `src/__tests__/kanban-family-registry-keys-retired-8257.test.ts` * (the key) and `src/__tests__/kanbanEnhancedRetired-8932.test.ts` (the file). diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index 2e4964f0be..109c75cdf8 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -57,7 +57,8 @@ import type { DASHBOARD_SPEC_EXCLUDED } from './zod/complex.zod.js'; * Every member below was carried from the plugin's declaration verbatim. The * runtime-computed members (`cardSubtitle`, `cardFieldCells`, `coverImage`, * a badge's `colorStyle`) are what `ObjectKanban` writes onto the cards it - * hands the board and what `KanbanImpl` / `KanbanEnhanced` read back; the Zod + * hands the board and what `KanbanImpl` reads back (the one board since + * objectui#8932 deleted `KanbanEnhanced`); the Zod * mirror (`zod/complex.zod.ts`) passes those through the way it passes * `TableColumn.headerIcon` through (objectui#6424). * @@ -128,9 +129,10 @@ export interface KanbanColumn { /** * Cards in this column. * - * Named `cards` because that is what every board reads and every authored - * document writes: `KanbanImpl` (12 lines), `KanbanEnhanced` (8) and - * `bucketCardsIntoColumns` all read `column.cards`, and the two catalog + * Named `cards` because that is what the board reads and every authored + * document writes: `KanbanImpl` and `bucketCardsIntoColumns` read + * `column.cards` (measured at objectui#6939: 12 lines in `KanbanImpl`, and 8 + * in `KanbanEnhanced` until objectui#8932 deleted it), and the two catalog * entries, the plugin docs and `content/docs/api/schema-reference.md` all * author it. The retired declarative face spelled this `items` until * objectui#6939 — a spelling with zero read sites, which made every authored diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index efe2595f4a..dba0fbefd1 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -3725,8 +3725,9 @@ export interface ObjectKanbanSchema extends BaseSchema { * ## ⛔ Why this is NOT `KanbanColumn[]`, which is what the card proposed * * {@link KanbanColumn} requires `cards`. It is the RUNTIME lane shape — what - * `bucketCardsIntoColumns` produces and what `KanbanImpl` / `KanbanEnhanced` - * consume, with `cards` always filled — and using it as the AUTHORING + * `bucketCardsIntoColumns` produces and what `KanbanImpl` consumes (the one + * board since objectui#8932 deleted `KanbanEnhanced`), with `cards` always + * filled — and using it as the AUTHORING * element would refuse three live shapes, each measured rather than argued: * * 1. the protocol's own gate-validated example @@ -3747,11 +3748,12 @@ export interface ObjectKanbanSchema extends BaseSchema { * ## The member set is the READ set * * `id` / `title` / `cards` / `limit` / `className` / `collapsed` are exactly - * the lane members the two board implementations read, counted off - * `KanbanImpl`, `KanbanEnhanced`, `useCrossSwimlaneMove`, + * the lane members the board reads, counted when there were two board + * implementations off `KanbanImpl`, `KanbanEnhanced`, `useCrossSwimlaneMove`, * `useQuickAddReorder` and `bucketCardsIntoColumns`. Their VALUE types come * from those read sites too — see {@link id} for the one this card had to - * correct. + * correct. objectui#8932 later deleted `KanbanEnhanced`; `KanbanImpl` alone + * still reads all six members. * * ## ⭐ The bare-string arm is REACHABLE since objectui#8990 * From 313ca6321590325182ae727320564fff3b772828 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 10:47:17 +0000 Subject: [PATCH 6/6] docs(changeset): scope the 8913 entry's "two board implementations" to its own change check-changeset-claims asked for this paragraph to be re-read because the branch now edits objectql.ts. The pending objectui#8913 entry says, in the present tense, that the lane members are exactly what "the two board implementations read". With KanbanEnhanced deleted, that is no longer true of what ships. The sentence is now scoped to "at this change", with a dated note in the objectui#10540 form. The frontmatter is byte-identical. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude --- .changeset/8913-object-kanban-columns-declared.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.changeset/8913-object-kanban-columns-declared.md b/.changeset/8913-object-kanban-columns-declared.md index 204704b3c4..1bc7ddff96 100644 --- a/.changeset/8913-object-kanban-columns-declared.md +++ b/.changeset/8913-object-kanban-columns-declared.md @@ -35,16 +35,21 @@ strings**. Both arms are admitted whole — `columns` is `string[] | Lane[]`, a union of two ARRAY shapes rather than an array of a per-element union — and on the lane arm **`cards` is optional**. The alternative — reusing `KanbanColumn`, whose `cards` is required — was measured and rejected: it is the RUNTIME lane -(`bucketCardsIntoColumns` fills `cards` before either board implementation sees -one), and as the authoring element it would have refused the protocol's own +(at this change, `bucketCardsIntoColumns` filled `cards` before either board +implementation saw one), and as the authoring element it would have refused the protocol's own gate-validated `{ id, title }` example, the lanes the renderer materializes from a picklist, and this repository's own typed board fixtures. **What is judged again.** A lane has `id` (**string**) and `title`, optionally carrying `cards`, `limit`, `className` and `collapsed` — exactly the members the -two board implementations read. When a lane carries `cards`, each card is judged -by the one card authority (`KanbanCardSchema`), which is what restores -objectui#6939's finding: **a card with no `title` is refused again**. +two board implementations read at this change. When a lane carries `cards`, each +card is judged by the one card authority (`KanbanCardSchema`), which is what +restores objectui#6939's finding: **a card with no `title` is refused again**. + +⚠️ **Dated note, 2026-09-25 — one board implementation remains — +objectui#8932.** Later in this same release `KanbanEnhanced`, the second of the +two, was deleted. `KanbanImpl` alone reads all six lane members, so the judged set +above is unchanged. The objectui#8932 entry states what ships. **Two shapes the protocol's literal `z.unknown()` would take are refused, because the renderer mishandles them.** A declaration that admits a shape nothing can