From 10340f22520e237951dfbc1290a537ac615639f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 04:29:07 +0000 Subject: [PATCH 1/2] fix(types,plugin-markdown,plugin-kanban): one authority for `MarkdownSchema` and the in-package Kanban pair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MarkdownSchema` converges onto `@object-ui/types`: the plugin's second declaration differed on one member (`content`, optional there and required in types), measured as drift rather than a semantic difference — the plugin's own registration marks the input `required: true` (test-pinned), its renderer props type is non-optional, the Zod mirror is `z.string()`, and every authored `type: 'markdown'` node supplies it. plugin-markdown now re-exports the one authority. `KanbanCard` / `KanbanColumn`: the three in-package declarations converge to one. An AST comparison found `KanbanImpl.tsx` and `KanbanEnhanced.tsx` strict subset copies of `./types` with nothing typed differently, so their extra members (`cardSubtitle`, `cardFieldCells`, `coverImage`, `collapsed` — all optional) move onto the canonical declaration and both files re-point at it, keeping their export surface via re-export. Baseline `one-authority-per-exported-name-6273`: `MarkdownSchema` row deleted; the `KanbanCard` / `KanbanColumn` rows shrink from four sites to the two cross-package ones, which are escalated rather than guessed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- .../6172-markdown-kanban-one-authority.md | 50 +++++++++++++++++ content/docs/plugins/plugin-markdown.mdx | 21 ++++--- packages/plugin-kanban/src/KanbanEnhanced.tsx | 29 +++------- packages/plugin-kanban/src/KanbanImpl.tsx | 46 +++------------ packages/plugin-kanban/src/types.ts | 28 ++++++++++ packages/plugin-markdown/src/types.ts | 56 ++++++++++--------- ...e-authority-per-exported-name-6273.test.ts | 27 +++++++-- 7 files changed, 161 insertions(+), 96 deletions(-) create mode 100644 .changeset/6172-markdown-kanban-one-authority.md diff --git a/.changeset/6172-markdown-kanban-one-authority.md b/.changeset/6172-markdown-kanban-one-authority.md new file mode 100644 index 0000000000..cbb6e6a6dd --- /dev/null +++ b/.changeset/6172-markdown-kanban-one-authority.md @@ -0,0 +1,50 @@ +--- +'@object-ui/plugin-markdown': minor +'@object-ui/plugin-kanban': minor +--- + +One authority for `MarkdownSchema`, and for `KanbanCard` / `KanbanColumn` +inside `@object-ui/plugin-kanban` (objectui#6172, folding in objectui#6155). + +The 2026-08-25 family ruling: every exported schema name has exactly one +authority. Two of this card's names are discharged here. + +**`MarkdownSchema` — converged onto `@object-ui/types`.** +`@object-ui/plugin-markdown` declared a second copy of the name. The two +differed on exactly one member — `content`, required in `@object-ui/types` and +optional in the plugin — and that was measured to be drift rather than a real +semantic difference: the plugin's own registration declares the `content` input +`required: true` (pinned by its own test), `MarkdownImplProps.content` is a +non-optional `string`, the Zod mirror spells `z.string()`, and every authored +`type: 'markdown'` node in the repository supplies `content`. The plugin now +re-exports the one authority. + +⚠️ **Breaking, in the narrowing direction, for `@object-ui/plugin-markdown` +consumers**: `MarkdownSchema['content']` goes from optional to **required**. A +value annotated `MarkdownSchema` that omitted `content` no longer type-checks. +Measured against this repository: zero authored markdown nodes omit it, so +nothing in-tree changed. (`type: 'markdown'` literals that carry no `content` +are rich-text FIELD metadata — `MarkdownFieldMetadata` — a different type.) +The plugin's face also gains the optional `sanitize` and `components` members +the canonical declaration carries; both are additive, and neither is read by +this renderer, which sanitizes unconditionally. + +`className` is unaffected — it comes from `BaseSchema`, which both copies +extended, so it was always inherited rather than added by the plugin. + +**`KanbanCard` / `KanbanColumn` — the three in-package copies converged to +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. + +Additive for consumers: `KanbanCard` gains `cardSubtitle`, `cardFieldCells` and +`coverImage`; `KanbanColumn` gains `collapsed`. All four are optional, so every +value that type-checked before still does. Both modules keep their previous +export surface via re-export, so no import path changes. + +The cross-package `KanbanCard` / `KanbanColumn` / `KanbanSchema` collision +between `@object-ui/types` and `@object-ui/plugin-kanban` is NOT resolved here +and is escalated on objectui#6172 — those are two different dialects (`items` +vs `cards`, `labels` vs `badges`), and collapsing them renames a published +name, which needs an authority ruling. diff --git a/content/docs/plugins/plugin-markdown.mdx b/content/docs/plugins/plugin-markdown.mdx index 05697cda88..4fb75dbc0c 100644 --- a/content/docs/plugins/plugin-markdown.mdx +++ b/content/docs/plugins/plugin-markdown.mdx @@ -50,23 +50,30 @@ const schema = { ```plaintext { type: 'markdown', - content?: string, // Markdown content + content: string, // Markdown content (required) className?: string // Tailwind classes } ``` ### MarkdownSchema -Declared by `@object-ui/plugin-markdown` -(`packages/plugin-markdown/src/types.ts`) — the plugin ships its own copy of -this name, so read it there rather than the same-named interface in -`@object-ui/types`. It extends `BaseSchema`, so the shared component properties -are available on a `markdown` node as well as the ones below. +Declared by `@object-ui/types` (`packages/types/src/data-display.ts`) and +re-exported by `@object-ui/plugin-markdown`, so either import spelling resolves +to the same type. The plugin used to ship a second declaration of this name; +the two have been converged onto the one authority (objectui#6172). It extends +`BaseSchema`, so the shared component properties are available on a `markdown` +node as well as the ones below. + +`content` is **required**: the plugin registers it as a required input, the +renderer's own props type takes a non-optional `string`, and the Zod mirror +(`packages/types/src/zod/data-display.zod.ts`) validates it as `z.string()`. | Property | Type | Default | Description | |----------|------|---------|-------------| -| `content` | string | `''` | Markdown content to render | +| `content` | string | — (required) | Markdown content to render | | `className` | string | `''` | Additional Tailwind CSS classes | +| `sanitize` | boolean | `true` | Declared on the schema; the renderer sanitizes unconditionally | +| `components` | Record | — | Declared on the schema; not read by this renderer | ## Supported Markdown Features diff --git a/packages/plugin-kanban/src/KanbanEnhanced.tsx b/packages/plugin-kanban/src/KanbanEnhanced.tsx index be463a32b0..3459972344 100644 --- a/packages/plugin-kanban/src/KanbanEnhanced.tsx +++ b/packages/plugin-kanban/src/KanbanEnhanced.tsx @@ -29,32 +29,17 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Butto 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(' ') -export interface KanbanCard { - id: string - title: string - description?: string - /** - * `colorStyle` carries the CSS custom properties a hex-derived `colorClass` - * reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to - * derive the pair. - */ - badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }> - coverImage?: string - [key: string]: any -} - -export interface KanbanColumn { - id: string - title: string - cards: KanbanCard[] - limit?: number - className?: string - collapsed?: boolean -} +// One authority for these two names, in `./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. diff --git a/packages/plugin-kanban/src/KanbanImpl.tsx b/packages/plugin-kanban/src/KanbanImpl.tsx index 5de9bc562e..77333ccd63 100644 --- a/packages/plugin-kanban/src/KanbanImpl.tsx +++ b/packages/plugin-kanban/src/KanbanImpl.tsx @@ -29,6 +29,7 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Scrol import { useHasDndProvider, useDnd, usePredicateScope } from "@object-ui/react" import { resolveConditionalFormatting } from "@object-ui/core" import type { KanbanConditionalFormattingRule } from "@object-ui/types" +import type { KanbanCard, KanbanColumn } from './types' import { createSafeTranslation } from "@object-ui/i18n" import { Plus } from "lucide-react" @@ -48,43 +49,14 @@ const useKanbanT = createSafeTranslation( const UNCATEGORIZED_LANE = 'Uncategorized' -export interface KanbanCard { - id: string - title: string - description?: string - /** - * Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered - * in preference to `description` so we don't have to overwrite the record's - * real `description` field — which would corrupt detail-view and edit-form - * displays once a card is opened. - */ - cardSubtitle?: string - /** - * Structured per-field cells. When provided, the card body renders each - * field via the unified `@object-ui/fields` cell-renderer pipeline (same - * as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields - * keep their semantic styling instead of being flattened to a text join. - * - * Takes precedence over `cardSubtitle` / `description` when present. - */ - cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }> - /** - * `colorStyle` carries the CSS custom properties a hex-derived `colorClass` - * reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to - * derive the pair. - */ - badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }> - coverImage?: string - [key: string]: any -} - -export interface KanbanColumn { - id: string - title: string - cards: KanbanCard[] - limit?: number - className?: string -} +// `KanbanCard` / `KanbanColumn` have ONE authority in this package: `./types` +// (objectui#6172 / #6155). This file used to redeclare both, and the copies had +// drifted — the local `KanbanCard` carried `cardSubtitle` / `cardFieldCells` / +// `coverImage` that `./types` did not. Those members moved to the canonical +// declaration (all optional, so nothing that type-checked before stopped), and +// the re-export below keeps this module's export surface byte-for-byte what it +// was for any importer. A re-export 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. diff --git a/packages/plugin-kanban/src/types.ts b/packages/plugin-kanban/src/types.ts index fa1b1dc006..b253d4b503 100644 --- a/packages/plugin-kanban/src/types.ts +++ b/packages/plugin-kanban/src/types.ts @@ -40,6 +40,29 @@ export interface KanbanCard { */ colorStyle?: React.CSSProperties; }>; + /** + * Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered + * in preference to `description` so we don't have to overwrite the record's + * real `description` field — which would corrupt detail-view and edit-form + * displays once a card is opened. + * + * Read by `KanbanImpl`; absent on a board that renders plain descriptions. + */ + cardSubtitle?: string; + /** + * Structured per-field cells. When provided, the card body renders each + * field via the unified `@object-ui/fields` cell-renderer pipeline (same + * as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields + * keep their semantic styling instead of being flattened to a text join. + * + * Takes precedence over `cardSubtitle` / `description` when present. + */ + cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>; + /** + * Resolved cover-image URL for the card, derived from the board's + * `coverImageField`. Read by both board implementations. + */ + coverImage?: string; [key: string]: any; } @@ -52,6 +75,11 @@ export interface KanbanColumn { cards: KanbanCard[]; limit?: number; className?: string; + /** + * Whether the lane renders collapsed. Honoured by `KanbanEnhanced` (the + * implementation that ships column collapsing); the plain board ignores it. + */ + collapsed?: boolean; } /** diff --git a/packages/plugin-markdown/src/types.ts b/packages/plugin-markdown/src/types.ts index e71cadc154..c14513f543 100644 --- a/packages/plugin-markdown/src/types.ts +++ b/packages/plugin-markdown/src/types.ts @@ -6,31 +6,35 @@ * LICENSE file in the root directory of this source tree. */ -import type { BaseSchema } from '@object-ui/types'; - /** - * Markdown component schema. - * Renders markdown content with GitHub Flavored Markdown support. + * `MarkdownSchema` has ONE authority: `@object-ui/types` (objectui#6172, the + * 2026-08-25 family ruling — every exported schema name has exactly one + * authority). This package used to declare a second copy of the name, and the + * two had drifted on exactly one member: `content` was REQUIRED there and + * optional here. + * + * That divergence was measured, not adjudicated by taste, and it was drift + * rather than a real semantic difference — every other statement this package + * makes about `content` already says required: + * + * - the registration in `./index.tsx` declares the `content` input + * `required: true`, and `./index.test.ts` pins that; + * - `MarkdownImplProps.content` in `./MarkdownImpl.tsx` is `content: string`, + * non-optional — the renderer that actually consumes it; + * - the Zod mirror `packages/types/src/zod/data-display.zod.ts` spells it + * `z.string()`, not `.optional()`, and a parity test pins it; + * - and every authored `type: 'markdown'` NODE in the repository supplies + * `content`. (The `type: 'markdown'` literals that omit it are rich-text + * FIELD metadata — `MarkdownFieldMetadata` in + * `packages/types/src/field-types.ts` — which is a different type.) + * + * The lone `content?: string` here was the outlier, so the copies converge onto + * the required spelling rather than the loose one. `className` is not lost: it + * is declared by `BaseSchema`, which both copies extend, so it was always + * inherited rather than added here. + * + * ⛔ Do not re-add a local `export interface MarkdownSchema`. A re-export is + * one declaration with many export sites; a second declaration is a second + * meaning behind one published name, which is the defect this converged. */ -export interface MarkdownSchema extends BaseSchema { - type: 'markdown'; - - /** - * The markdown content to render. Supports GitHub Flavored Markdown including: - * - Headers (H1-H6) - * - Bold, italic, and inline code - * - Links and images - * - Lists (ordered, unordered, and nested) - * - Tables - * - Blockquotes - * - Code blocks - * - Strikethrough - * - Task lists - */ - content?: string; - - /** - * Optional CSS class name to apply custom styling to the markdown container. - */ - className?: string; -} +export type { MarkdownSchema } from '@object-ui/types'; 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 c71aac9e30..a27f1f2c82 100644 --- a/scripts/__tests__/one-authority-per-exported-name-6273.test.ts +++ b/scripts/__tests__/one-authority-per-exported-name-6273.test.ts @@ -394,10 +394,29 @@ const KNOWN_COLLISIONS: ReadonlyMap = new Map([ // authority left the tree with the spec-bridge retirement (objectui#6366, // 2026-08-27 maintainer ruling), so app-shell's `form-spec.ts` is now the // one authority and the entries would fail the stale-baseline direction. - ['KanbanCard', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6155 — the ×4 that card measured - ['KanbanColumn', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // the same four files; no family card named it - ['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172 - ['MarkdownSchema', ['packages/plugin-markdown/src/types.ts', 'packages/types/src/data-display.ts']], // objectui#6172 + // `KanbanCard` / `KanbanColumn` were the ×4 objectui#6155 measured. The THREE + // in-package copies converged (objectui#6172): `KanbanImpl.tsx` and + // `KanbanEnhanced.tsx` were strict-SUBSET copies of `./types` — an AST probe + // found nothing typed differently between them — so their members (`coverImage`, + // `cardSubtitle`, `cardFieldCells`, `collapsed`, all optional) moved onto the one + // in-package declaration and both files now re-point at it. Two sites remain, and + // they are the CROSS-package pair: the `@object-ui/types` copy is a different + // dialect (`items` where the plugin says `cards`, `labels` where it says + // `badges`), so collapsing it is a rename of a published name and needs an + // authority ruling the 2026-08-25 family ruling did not give for this pair — it + // named one only for the cross-package `FormField` clash. Escalated, not guessed. + ['KanbanCard', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], + ['KanbanColumn', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], + ['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172 — same cross-package pair, same escalation + // `MarkdownSchema` sat here, colliding between + // `packages/plugin-markdown/src/types.ts` and `packages/types/src/data-display.ts`. + // The copies differed on ONE member — `content`, required there and optional here — + // and that was measured to be DRIFT rather than a semantic difference: the plugin's + // own registration declares the `content` input `required: true` (pinned by its own + // test), its `MarkdownImplProps.content` is non-optional, the Zod mirror spells + // `z.string()`, and every authored `type: 'markdown'` NODE in the repo supplies it. + // So plugin-markdown re-points at the one authority in `@object-ui/types` + // (objectui#6172). ['MenuItem', ['packages/types/src/app.ts', 'packages/types/src/overlay.ts']], ['MetadataTypeStatus', ['packages/app-shell/src/providers/MetadataProvider.tsx', 'packages/react/src/context/AppShellContext.tsx']], ['NamedActionDef', ['packages/plugin-grid/src/resolveBulkActions.ts', 'packages/plugin-grid/src/resolveLegacyRowActions.ts']], From 892625fb72641a5c27b4b1715a9b0f7b1b6d6ae3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 05:53:08 +0000 Subject: [PATCH 2/2] docs(plugin-markdown,plugin-kanban): require `content` in the README, scope the Kanban authority note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1: the "Schema API" block in packages/plugin-markdown/README.md still spelled `content` as optional — the exact outlier spelling this branch removes, in a doc that ships in the npm tarball. The canonical declaration is non-optional: `content: z.string()` at packages/types/src/zod/data-display.zod.ts:276 and `content: string` at packages/plugin-markdown/src/MarkdownImpl.tsx:31. No gate catches this; check-readme-exports judges export existence, not shape prose. C2: KanbanEnhanced.tsx's authority comment now carries the "in this package" qualifier its KanbanImpl twin already spells. The cross-package Kanban pair is deliberately open, so the unqualified wording overstated what this branch settles. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- packages/plugin-kanban/src/KanbanEnhanced.tsx | 2 +- packages/plugin-markdown/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-kanban/src/KanbanEnhanced.tsx b/packages/plugin-kanban/src/KanbanEnhanced.tsx index 3459972344..c582011172 100644 --- a/packages/plugin-kanban/src/KanbanEnhanced.tsx +++ b/packages/plugin-kanban/src/KanbanEnhanced.tsx @@ -34,7 +34,7 @@ import { ChevronDown, ChevronRight, AlertTriangle, Plus } from "lucide-react" const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ') -// One authority for these two names, in `./types` (objectui#6172 / #6155). +// 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 diff --git a/packages/plugin-markdown/README.md b/packages/plugin-markdown/README.md index 8661c12954..be10e1069b 100644 --- a/packages/plugin-markdown/README.md +++ b/packages/plugin-markdown/README.md @@ -62,7 +62,7 @@ const schema: MarkdownSchema = { ```typescript { type: 'markdown', - content?: string, // Markdown content (supports GitHub Flavored Markdown) + content: string, // Markdown content (supports GitHub Flavored Markdown) className?: string // Tailwind classes } ```