From 5572f48ba52809c8f0ada949dd930006911166ca Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 16:12:27 +0000 Subject: [PATCH 1/2] docs(skills): judge each of the ten governed fences that shadowed a published type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five fences stop keeping a private copy of a published `@object-ui` type, four keep a deliberately simplified teaching copy and now say so, and one is a prose repair for a type that is not on the published surface at all. Per-fence judgement, never a sweep: a guide's fence is often a short view of a large type, and converting those wholesale would make several guides worse. Import (the copy was already the whole type, or was wrong about it): auth-permissions.md AuthUser — the copy still taught the `roles` member ADR-0090 D3 renamed to `positions` data-integration.md QueryParams — the copy invented an index signature the published type does not have data-integration.md QueryResult — the copy was the whole type, member for member plugin-development.md ComponentInput, ComponentInputControlType — the copy declared `label`, `defaultValue` and `advanced` writable where all three are `never` tombstones, and carried no `of` Derive under `Pick` (a short shape anchored to the real one): plugin-development.md FieldWidgetComponentProps — the seven props an author writes, out of a much larger closed type Keep the teaching copy, and say so (published type too large to open with): architecture.md BaseSchema (21 members + index signature) protocol.md BaseSchema data-integration.md DataSource (38 members) console-development.md MetadataResourceConfig (28 members) Prose only, no import exists to write: console-development.md NavigationContextType — internal to `packages/app-shell` and not re-exported from its entry Both shrink-only ledgers in `scripts/check-skill-examples.mjs` shrink to empty in this same commit, because a row that outlives its fence fails as STALE: the four `KNOWN_SHADOWED_PUBLISHED_TYPES` rows whose three fences now import, and the last `KNOWN_BARE_ANY_EXAMPLES` row, whose `any` went with the copy that carried it. No assertion, threshold or list shape changed. Claude-Session: https://claude.ai/code/session_018dxq7YqsLDMeZDZ5AzsgJX --- .../guides/console-development.md | 17 +++- scripts/check-skill-examples.mjs | 58 ++++++------ skills/objectui/guides/architecture.md | 10 ++ skills/objectui/guides/auth-permissions.md | 32 +++++-- skills/objectui/guides/data-integration.md | 65 ++++++++----- skills/objectui/guides/plugin-development.md | 94 ++++++++++++------- skills/objectui/rules/protocol.md | 9 ++ 7 files changed, 190 insertions(+), 95 deletions(-) diff --git a/.claude/skills/objectui-contributor/guides/console-development.md b/.claude/skills/objectui-contributor/guides/console-development.md index 27e5f4625..6cdbbb058 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 27 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 e0a3f90e6..767c6ccc2 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 dfb1bd490..95e29fb44 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 61fbd1bef..515f67eb7 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 22ee8403c..62739ad66 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 2bcee1b72..83a19876c 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 3d5cf0fa5..b2680d9f5 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 From 3fd0e3737731147aaed143dc22b5f87f88892833 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 22:39:13 +0000 Subject: [PATCH 2/2] docs(skills): say 28 where the console guide's own arithmetic already said 28 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The paragraph above the MetadataResourceConfig fence gave two different totals for the same interface in the same breath: "the 22 fields you reach for most out of the 28 the interface declares ... registry.ts documents all 27 field by field", with six unshown fields named (22 + 6 = 28). The 27 was a stale number from an earlier count, left behind when the AST measurement corrected it. Re-measured on this branch: the interface has 28 members and registry.ts carries a doc comment on every one of them, so "all 28 field by field" is the reading. Verified by walking the members with the TypeScript AST and checking each one's leading trivia — 28 documented, 0 undocumented. Claude-Session: https://claude.ai/code/session_018dxq7YqsLDMeZDZ5AzsgJX --- .../skills/objectui-contributor/guides/console-development.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/objectui-contributor/guides/console-development.md b/.claude/skills/objectui-contributor/guides/console-development.md index 6cdbbb058..26f17e1e3 100644 --- a/.claude/skills/objectui-contributor/guides/console-development.md +++ b/.claude/skills/objectui-contributor/guides/console-development.md @@ -154,7 +154,7 @@ SIMPLIFIED teaching copy of the published `MetadataResourceConfig` 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 27 field by field, and the six not shown here are `hiddenFields`, +all 28 field by field, and the six not shown here are `hiddenFields`, `fieldOrder`, `createSeed`, `createSchema`, `createHint` and `emptyStateHint`. ```typescript