diff --git a/.claude/skills/objectui-contributor/guides/console-development.md b/.claude/skills/objectui-contributor/guides/console-development.md index 27e5f46258..26f17e1e38 100644 --- a/.claude/skills/objectui-contributor/guides/console-development.md +++ b/.claude/skills/objectui-contributor/guides/console-development.md @@ -148,8 +148,14 @@ The contract for "add a new metadata type" is therefore mostly *not* a UI change ### MetadataResourceConfig — the override surface -Every field is optional. The ones you reach for most (see `registry.ts` for the full -~30-field interface, which is documented field by field): +Every field except `type` is optional. The fence below is a deliberately +SIMPLIFIED teaching copy of the published `MetadataResourceConfig` +(`import type { MetadataResourceConfig } from '@object-ui/app-shell';`), KEPT +rather than replaced by an import: it shows the 22 fields you reach for most out +of the **28** the interface declares, and the excerpt is what makes the section +usable. Nothing checks the copy against the real type — `registry.ts` documents +all 28 field by field, and the six not shown here are `hiddenFields`, +`fieldOrder`, `createSeed`, `createSchema`, `createHint` and `emptyStateHint`. ```typescript interface MetadataResourceConfig { @@ -280,6 +286,13 @@ item, or nothing at all is the answer more often than a bespoke page is. type NavigationContextType = 'home' | 'app'; ``` +⚠️ Unlike every other type quoted in this guide, `NavigationContextType` is +INTERNAL: `@object-ui/app-shell` exports only `.` (→ `dist/index.d.ts`) and +`./styles.css`, and its entry re-exports `useNavigationContext` but not this +type. So there is no import a consumer can write for it — a contributor reaches +it through the source path above, and the fence is a quotation of that file +rather than a copy standing in for a published name. + - `HomeLayout` sets context to `'home'` → sidebar shows workspace nav - `ConsoleLayout` sets context to `'app'` → sidebar shows app-specific nav diff --git a/scripts/check-skill-examples.mjs b/scripts/check-skill-examples.mjs index e0a3f90e69..767c6ccc20 100644 --- a/scripts/check-skill-examples.mjs +++ b/scripts/check-skill-examples.mjs @@ -592,13 +592,8 @@ const REMEDY_FLOOR = * would keep covering a site nobody has looked at since. * * The corpus at objectui#7463's branch point was FOUR rows, measured under - * `--measure` before the assertion was armed. One is left, and it is not a - * mechanical unmark: - * - * - `plugin-development.md:92` `defaultValue` — the guide is FAITHFUL prose, - * not rot. `ComponentInput.defaultValue` really is `any` in - * `packages/types/src/base.ts`, so the honest fix is to the platform type, - * not to the guide restating it. Fixing the guide alone would make it lie. + * `--measure` before the assertion was armed. All four are retired, and none of + * them by a mechanical unmark. * * The three `testing.md` rows were retired by objectui#7494, which answered the * skills judgement each of them was waiting on and taught the honest idiom in @@ -608,11 +603,19 @@ const REMEDY_FLOOR = * field through a cast. Both fences stay MARKED; the rows went in the same * commit as the guide edit, because a row whose red is gone fails as STALE. * + * The fourth — `plugin-development.md:92` `defaultValue` — was retired the same + * way by the fence repair. Its reason had gone out of date under it: the row + * said the guide was FAITHFUL prose because `ComponentInput.defaultValue` + * "really is `any`", and it is not — the platform type has carried it as an + * ADR-0049 retirement tombstone (`defaultValue?: never`) for some time, so the + * guide's `any` was rot after all, and the honest fix WAS to the guide. That + * fence now imports `ComponentInput` instead of re-declaring it, which removes + * the `any` along with the copy. The list is empty; an empty shrink-only list is + * the terminal state it is shaped for, not a disabled one. + * * @type {ReadonlySet} */ -export const KNOWN_BARE_ANY_EXAMPLES = new Set([ - 'skills/objectui/guides/plugin-development.md:92 property `defaultValue`', -]); +export const KNOWN_BARE_ANY_EXAMPLES = new Set([]); /** The baseline key for one bare-`any` finding at one site. */ export function bareAnyRowKey(block, finding) { @@ -959,30 +962,25 @@ export function shadowCandidates(blocks) { * because the name is IN the key: the old row goes stale and the new name * arrives undeclared, so one edit reds twice and says both halves. * - * The three rows below were measured under `--measure` on this card's branch + * Four rows were seeded here, measured under `--measure` on this gate's branch * point (`fedfa3e4a`), where the census found 3 of the 13 marked fences - * shadowing a published name — 23% of everything this gate judges. + * shadowing a published name — 23% of everything this gate judges. ALL FOUR ARE + * RETIRED: objectui#8335 gave each of those three fences one of the per-fence + * judgements objectui#7646's triage laid down, and all three came out as + * IMPORT, in the same commit as this list shrank to nothing. What the rows + * bought was measurable at the moment they went: each of the three copies had + * drifted, and two of them held claims that were flatly false against the + * published surface — `AuthUser` still taught a `roles` member ADR-0090 D3 had + * renamed away, and `ComponentInput` declared `label`, `defaultValue` and + * `advanced` as writable where all three are `never` tombstones. + * + * An EMPTY list is the terminal state this shape is for, not a disabled one: + * the assertion is unchanged and a new offender still reds on sight. The next + * marked fence that re-declares a published name has no row to hide behind. * * @type {ReadonlyMap} */ -export const KNOWN_SHADOWED_PUBLISHED_TYPES = new Map([ - [ - 'skills/objectui/guides/auth-permissions.md:59 AuthUser', - "the section is titled '### AuthUser type' and then re-declares it instead of importing it from @object-ui/auth; repair owned by objectui#8335 (governed surface, human merge)", - ], - [ - 'skills/objectui/guides/data-integration.md:81 QueryResult', - "the section is titled '### QueryResult' and then re-declares it instead of importing it from @object-ui/types; repair owned by objectui#8335 (governed surface, human merge)", - ], - [ - 'skills/objectui/guides/plugin-development.md:92 ComponentInput', - 'the fence documents ComponentInput including its five ADR-0049 retirement tombstones and is the specimen objectui#7636 paid for — a private copy that was WRONG and green for its whole life; repair owned by objectui#8335 (governed surface, human merge)', - ], - [ - 'skills/objectui/guides/plugin-development.md:92 ComponentInputControlType', - 'the same fence re-declares the control-type union ComponentInput.type is annotated with; repair owned by objectui#8335 (governed surface, human merge)', - ], -]); +export const KNOWN_SHADOWED_PUBLISHED_TYPES = new Map([]); /** The ledger key for one shadowed published name in one fence. */ export function shadowedTypeRowKey(block, name) { diff --git a/skills/objectui/guides/architecture.md b/skills/objectui/guides/architecture.md index dfb1bd4900..95e29fb44e 100644 --- a/skills/objectui/guides/architecture.md +++ b/skills/objectui/guides/architecture.md @@ -17,6 +17,16 @@ integration package to install. Day-to-day schema authoring is Every node in the UI tree follows this shape — enforce it on every input. +⚠️ The fence below is a deliberately SIMPLIFIED teaching copy of the published +`BaseSchema` (`import type { BaseSchema } from '@object-ui/types';`), KEPT +rather than replaced by an import: the published interface declares **21** +members plus an index signature, and this section teaches the SHAPE, not the +member list. Two consequences before you go looking in the `.d.ts`: nothing +checks the copy against the real type, and +`props` / `events` below are **not declared members** — they reach a node +through `BaseSchema`'s `[key: string]: any` index signature, which is why you +will not find them in the published member list. + ```typescript // @object-ui/types — abridged; the full BaseSchema member list is packages/types/src/base.ts interface BaseSchema { diff --git a/skills/objectui/guides/auth-permissions.md b/skills/objectui/guides/auth-permissions.md index 61fbd1bef3..515f67eb73 100644 --- a/skills/objectui/guides/auth-permissions.md +++ b/skills/objectui/guides/auth-permissions.md @@ -57,18 +57,32 @@ function UserBadge() { ```typescript -interface AuthUser { - id: string; - name: string; - email: string; - image?: string; - role?: string; - roles?: string[]; - emailVerified?: boolean; - [key: string]: unknown; +import type { AuthUser } from '@object-ui/auth'; + +// Imported, not re-declared. The old fence here kept a private copy of this +// type; the copy could not drift loudly, so it went on teaching a member the +// package had already retired. An import cannot do that: a member that goes +// away upstream stops compiling here on the day it goes. +function isTenantAdmin(user: AuthUser): boolean { + return user.tenantId !== undefined && (user.positions ?? []).includes('admin'); } ``` +`AuthUser` extends the spec's `IAuthService` principal — `id`, `email`, `name`, +plus the `positions` / `tenantId` an authorization decision reads — and adds the +display-only fields better-auth returns to the client: `image`, `role`, +`emailVerified`. The index signature (`[key: string]: unknown`) is deliberate: +better-auth projects an app's own user columns onto this object, and no local +type can enumerate them. + +⛔ **There is no `roles` member.** The hand-copied `roles?: string[]` mirror was +RETIRED with objectui#5424 (maintainer ruling 2026-08-22) once its last reader — +`AuthGuard`'s `requiredRoles` check — moved to `positions`. Framework ADR-0090 +D3 renamed `roles` → `positions` with no deprecation window, and the protocol-17 +session face emits no `roles` key at all. Reading `user.roles` now resolves +through the index signature as `unknown` rather than as `string[]`. ⛔ Do not +re-declare it, and do not re-emit it as a compatibility shadow of `positions`. + ### AuthGuard (route protection) ```typescript diff --git a/skills/objectui/guides/data-integration.md b/skills/objectui/guides/data-integration.md index 22ee8403c2..62739ad66f 100644 --- a/skills/objectui/guides/data-integration.md +++ b/skills/objectui/guides/data-integration.md @@ -22,7 +22,12 @@ Components never import fetch libraries directly. They access data through `useD ## DataSource interface -Defined in `packages/types/src/data.ts`: +⚠️ The fence below is a deliberately SIMPLIFIED teaching copy, kept rather than +replaced by an import: the published `DataSource` has **38** members and the +excerpt is what makes this section readable. The real one is +`import type { DataSource } from '@object-ui/types';` (source: +`packages/types/src/data.ts`) — read it before you rely on any member's exact +signature, because nothing checks the copy below against it. ```typescript interface DataSource { @@ -61,35 +66,53 @@ that do **not** exist: there is no `saveView` (write through `updateViewConfig` ### QueryParams ```typescript -interface QueryParams { - $select?: string[]; // SELECT specific fields - $filter?: Record | FilterArray; // WHERE conditions (FilterArray: @objectstack/spec/data) - $orderby?: string | Record | string[] | Array<{ field: string; order?: 'asc' | 'desc' }>; - $skip?: number; // OFFSET (for pagination) - $top?: number; // LIMIT (page size) - $expand?: string[]; // JOIN/expand related objects - $search?: string; // free-text search term - $searchFields?: string[]; // fields the search term is matched against - $count?: boolean; // ask the backend for `total` - [key: string]: any; // why an unprefixed `limit` type-checks — and is then dropped -} +import type { QueryParams } from '@object-ui/types'; + +// Imported, not re-declared: the nine keys below are the whole published type, +// so a private copy could only be a second, slower-moving answer. +const params: QueryParams = { + $select: ['name', 'email'], // SELECT specific fields + $filter: { status: 'active' }, // WHERE conditions (or a FilterArray from @objectstack/spec/data) + $orderby: { createdAt: 'desc' }, + $skip: 0, // OFFSET (for pagination) + $top: 20, // LIMIT (page size) + $expand: ['owner'], // JOIN/expand related objects + $search: 'acme', // free-text search term + $searchFields: ['name'], // fields the search term is matched against + $count: true, // ask the backend for `total` +}; ``` +`$orderby` also takes a `string`, a `string[]`, or an +`Array<{ field: string; order?: 'asc' | 'desc' }>` — read the published type for +the full union rather than copying this one arm. + +⚠️ `QueryParams` declares those nine keys and **no index signature**. So an +unprefixed `limit` is a compile error at the call site, not a key that +type-checks and is then dropped on the wire. This fence claimed the opposite +while it kept its own copy — which is exactly the drift a copy cannot report. + ### QueryResult ```typescript -interface QueryResult { - data: T[]; // Returned data array — required, and NOT named `records` - total?: number; // Total count for pagination - page?: number; // Current page (1-indexed) - pageSize?: number; // Items per page - hasMore?: boolean; // Cursor-based pagination flag - cursor?: string; // Next page cursor - metadata?: Record; // Additional metadata +import type { QueryResult } from '@object-ui/types'; + +// Imported, not re-declared. The fence's old copy was already the WHOLE type, +// member for member, so the copy bought the reader nothing and could only drift. +function summarize(result: QueryResult<{ id: string }>) { + return { + items: result.data, // required, and NOT named `records` + total: result.total ?? result.data.length, + nextCursor: result.hasMore ? result.cursor : undefined, + }; } ``` +The whole type is seven members: `data` (required), `total`, `page` +(1-indexed), `pageSize`, `hasMore`, `cursor`, `metadata`. `data` is the one a +mis-remembering adapter gets wrong — it is **not** named `records`. + ## Wiring DataSource to SchemaRenderer ### Basic setup diff --git a/skills/objectui/guides/plugin-development.md b/skills/objectui/guides/plugin-development.md index 2bcee1b720..83a19876c8 100644 --- a/skills/objectui/guides/plugin-development.md +++ b/skills/objectui/guides/plugin-development.md @@ -90,34 +90,51 @@ Sixteen keys in total: the eleven on `ComponentMeta` (`@object-ui/types` ```typescript -type ComponentInputControlType = - | 'string' | 'number' | 'boolean' | 'enum' | 'array' | 'object' - | 'color' | 'date' | 'code' | 'file' | 'slot'; - -type ComponentInput = { - name: string; // Maps to component prop +import type { ComponentInput, ComponentInputControlType } from '@object-ui/types'; + +// The eleven control kinds, taken from the published union rather than retyped. +const controlTypes: ComponentInputControlType[] = [ + 'string', 'number', 'boolean', 'enum', 'array', 'object', + 'color', 'date', 'code', 'file', 'slot', +]; + +// A manifest's inputs, checked against the published type. SIX keys are +// writable — `name`, `type`, `of`, `required`, `enum`, `description`. +const inputs: ComponentInput[] = [ + { name: 'title', type: 'string', required: true, description: 'Heading text' }, // ONE control type, or an ARRAY of them when the input accepts several // shapes (objectui#3832). Widening the vocabulary is a contract change. - type: ComponentInputControlType | ComponentInputControlType[]; - label?: string; - defaultValue?: any; - required?: boolean; - enum?: string[] | Array<{ label: string; value: string }>; - description?: string; // Also where a control hint or a numeric domain goes - advanced?: boolean; // Hide by default in designer - - // ADR-0049 RETIREMENT TOMBSTONES (objectui#5905) - DECLARED but UNWRITABLE. - // Authoring one is a `tsc` error here and a named refusal from the Zod - // mirror (`ComponentInputSchema`). They were never read and never published: - // the manifest serializer forwards only `name`, `type`, `required`, `enum`, - // `binding` and `description`, so an authored value was silently dropped. - // Remedy: delete the key and say it in `description`, which IS published. - inputType?: never; - min?: never; max?: never; step?: never; - placeholder?: never; // `BaseSchema.placeholder` is a different key, alive -}; + { name: 'items', type: ['array', 'object'], of: 'object', description: 'Rows to render' }, + { name: 'variant', type: 'enum', enum: ['solid', 'outline'], description: 'Visual style' }, +]; + +// ADR-0049 RETIREMENT TOMBSTONES (objectui#5905) - DECLARED but UNWRITABLE. +// Every key named here is `never` on the published type, so authoring one is a +// `tsc` error and a named refusal from the Zod mirror (`ComponentInputSchema`). +// They were never read and never published: the manifest serializer forwards +// only `name`, `type`, `required`, `enum`, `binding` and `description`, so an +// authored value was silently dropped. Remedy: delete the key and say it in +// `description`, which IS published. A `Pick` rather than a prose list, so a +// tombstone that is deleted upstream stops compiling here instead of lingering. +type ComponentInputTombstones = Pick< + ComponentInput, + | 'label' + | 'defaultValue' + | 'advanced' + | 'inputType' + | 'min' + | 'max' + | 'step' + | 'placeholder' // `BaseSchema.placeholder` is a different key, alive +>; ``` +⛔ This fence used to keep a private copy of both names, and the copy was wrong +in four ways at once while every gate stayed green: it declared `label`, +`defaultValue` and `advanced` as WRITABLE where the published type has all three +as `never` tombstones, and it did not carry `of` at all. That is what a +teaching copy of a contract type costs — objectui#7636 paid it first. + ### Looking up components ```typescript @@ -272,18 +289,29 @@ export function ColorField({ ### FieldWidgetComponentProps interface +The seven props a widget author writes, DERIVED from the published type rather +than retyped: the key names are checked against it and the value types come +from it, so this short view cannot drift from the long one. + ```typescript -type FieldWidgetComponentProps = { - value: T; // Current field value - onChange: (val: T) => void; // Value change callback - field: FieldMetadata; // Field metadata (name, label, type, etc.) - readonly?: boolean; // Read-only mode - disabled?: boolean; // HTML disabled state - className?: string; // Tailwind CSS classes - error?: string; // Active validation message — drive `aria-invalid` with it -}; +import type { FieldWidgetComponentProps } from '@object-ui/fields'; + +type WidgetAuthorProps = Pick< + FieldWidgetComponentProps, + | 'value' // Current field value — typed `T` + | 'onChange' // Value change callback — `(val: T) => void` + | 'field' // Field metadata (name, label, type, etc.) + | 'readonly' // Read-only mode + | 'disabled' // HTML disabled state + | 'className' // Tailwind CSS classes + | 'error' // Active validation message — drive `aria-invalid` with it +>; ``` +A widget signature normally names the whole type, as the example above does +(`FieldWidgetComponentProps`); the `Pick` here exists to show WHICH +seven a widget author actually writes without keeping a second copy of them. + The slot is named `error` because that is what `FieldWidgetPropsSchema` in `@objectstack/spec/ui` — the published widget contract — calls it. diff --git a/skills/objectui/rules/protocol.md b/skills/objectui/rules/protocol.md index 3d5cf0fa5c..b2680d9f5a 100644 --- a/skills/objectui/rules/protocol.md +++ b/skills/objectui/rules/protocol.md @@ -45,6 +45,15 @@ These top-level schema fields are passed as raw strings: Every UI component node MUST follow this shape: +⚠️ A deliberately SIMPLIFIED teaching copy of the published `BaseSchema` +(`import type { BaseSchema } from '@object-ui/types';`), KEPT rather than +replaced by an import: the published interface declares **21** members plus an +index signature, and this rule is about the SHAPE, not the full member list. +Nothing checks the copy against the real type. `properties`, `props` and +`events` are **not declared members** — they reach a node through +`BaseSchema`'s `[key: string]: any` index signature, which is exactly why the +two rules below have to state who reads each of them. + ```typescript interface BaseSchema { // abridged — full member list: packages/types/src/base.ts type: string; // Required: component type identifier