From 755872fa11e010b5c12bc2af64c52cb35708b89d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 06:36:31 +0000 Subject: [PATCH 1/5] feat(spec): retire the themes carrier key and ThemeSchema (#10485, ADR-0049) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruled B (退役授权面, 2026-08-21): strict-delete the stack themes key with a guidance prescription, delete ui/theme.zod.ts whole, drop the ingest mapping and the PLURAL_TO_SINGULAR fold (so /meta/theme gets the #8421 refusal), register six retired-def entries and the D3 semantic entry stack-themes-carrier-retired, pin the refusal, re-base docs and the showcase on app.branding. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw --- .changeset/stack-themes-carrier-retired.md | 86 ++ content/docs/getting-started/examples.mdx | 2 +- .../docs/getting-started/quick-reference.mdx | 2 +- content/docs/getting-started/quick-start.mdx | 3 +- .../protocol/objectui/widget-contract.mdx | 67 +- content/docs/references/index.mdx | 9 +- content/docs/references/ui/index.mdx | 1 - content/docs/references/ui/meta.json | 1 - content/docs/references/ui/theme.mdx | 144 ---- content/docs/ui/index.mdx | 4 +- content/docs/ui/react-pages.mdx | 6 +- ...07-unknown-key-strictness-ledger.counts.md | 13 +- examples/app-showcase/objectstack.config.ts | 3 +- examples/app-showcase/src/coverage.ts | 4 +- .../app-showcase/src/docs/showcase_tour_ui.md | 7 +- examples/app-showcase/src/ui/themes/index.ts | 34 - packages/cli/src/commands/explain.ts | 5 +- packages/cli/src/utils/format.ts | 3 - ...col.meta-types-mint-door-agreement.test.ts | 44 +- .../protocol.unrecognised-meta-type.test.ts | 37 +- packages/metadata/src/plugin.ts | 5 +- .../src/additional-domains.fixtures.ts | 16 +- .../downstream-contract/test/contract.test.ts | 4 +- packages/spec/api-surface-signatures.json | 1 - packages/spec/api-surface/root.json | 1 - packages/spec/api-surface/ui.json | 14 - packages/spec/authorable-defaults/ui.json | 1 - packages/spec/authorable-surface/ui.json | 57 -- packages/spec/export-origins/root.json | 1 - packages/spec/export-origins/ui.json | 14 - packages/spec/json-schema.manifest/ui.json | 6 - .../spec/scripts/build-skill-references.ts | 2 +- .../src/automation/sync-retirement.test.ts | 3 +- packages/spec/src/index.ts | 3 +- .../spec/src/kernel/metadata-type-schemas.ts | 26 +- .../meta-spelling/meta-url-data.generated.ts | 1 - .../meta-spelling/metadata-url-spelling.ts | 22 +- .../retired-defs/18.ui__BorderRadius.ts | 7 + .../retired-defs/18.ui__ColorPalette.ts | 7 + .../entries/retired-defs/18.ui__Shadow.ts | 7 + .../entries/retired-defs/18.ui__Theme.ts | 20 + .../entries/retired-defs/18.ui__ThemeMode.ts | 7 + .../entries/retired-defs/18.ui__Typography.ts | 7 + .../18.stack-themes-carrier-retired.ts | 50 ++ packages/spec/src/migrations/registry.ts | 102 ++- .../src/shared/metadata-collection.zod.ts | 10 +- .../spec/src/stack-top-level-strict.test.ts | 48 ++ packages/spec/src/stack.zod.ts | 21 +- .../src/type-alias-convention.pin.test.ts | 11 +- packages/spec/src/ui/index.ts | 12 +- .../ui/interaction-config-retirement.test.ts | 5 +- packages/spec/src/ui/theme.test.ts | 810 ------------------ packages/spec/src/ui/theme.zod.ts | 503 ----------- .../src/ui/widget-i18n-retirement.test.ts | 2 +- scripts/check-stack-collection-maps.mjs | 9 +- skills/objectstack-platform/SKILL.md | 2 +- skills/objectstack-ui/references/_index.md | 1 - 57 files changed, 537 insertions(+), 1756 deletions(-) create mode 100644 .changeset/stack-themes-carrier-retired.md delete mode 100644 content/docs/references/ui/theme.mdx delete mode 100644 examples/app-showcase/src/ui/themes/index.ts create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.ui__BorderRadius.ts create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.ui__ColorPalette.ts create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.ui__Shadow.ts create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.ui__Theme.ts create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.ui__ThemeMode.ts create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.ui__Typography.ts create mode 100644 packages/spec/src/migrations/entries/semantic/18.stack-themes-carrier-retired.ts delete mode 100644 packages/spec/src/ui/theme.test.ts delete mode 100644 packages/spec/src/ui/theme.zod.ts diff --git a/.changeset/stack-themes-carrier-retired.md b/.changeset/stack-themes-carrier-retired.md new file mode 100644 index 0000000000..9d715ee2b1 --- /dev/null +++ b/.changeset/stack-themes-carrier-retired.md @@ -0,0 +1,86 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec): retire the `themes` carrier key and `ThemeSchema` — the authoring surface nothing ever applied (#10485, ADR-0049) + +**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep +launch-window convention ships it as `minor`; the migration prescription is +registered under protocol major 18, where `os migrate meta` users will look). +Maintainer ruling 2026-08-21, recorded verbatim on #10485: 「B:退役授权面 — +收掉 `themes` 载体键与 schema,`app.branding` 留作唯一颜色面;objectui 引擎代码 +与单测保留。」 + +`defineStack({ themes })` was a real authoring surface — parsed strictly at the +authoring gate, ingested and stored by artifact ingest +(`ARTIFACT_FIELD_TO_TYPE`) — with ZERO consumers past that point, measured: +no non-test read of `.themes` or of stored `theme` items anywhere in +core/runtime/rest/services/plugins; `theme` never in `MetadataTypeSchema`, +`DEFAULT_METADATA_TYPE_REGISTRY` or `BUILTIN_METADATA_TYPE_SCHEMAS`; the only +mounted `ThemeProvider` is the app-shell chrome light/dark toggle (unrelated to +`ThemeSchema`); and no stack- or app-level key ever selected an active theme. +An author who wrote a theme shipped it through every green gate and the console +looked exactly the same. + +**What is refused:** the top-level `themes:` key. `ObjectStackDefinitionSchema` +is a `strictObject`, so the key is deleted from the shape and the unknown-key +rejection carries the retirement prescription via the schema's `guidance` entry +(removal citation, why it was inert, and the `app.branding` replacement). +`ThemeSchema`, `ColorPaletteSchema`, `TypographySchema`, `BorderRadiusSchema`, +`ShadowSchema`, `ThemeModeSchema`, `defineTheme` and the `Theme` / +`ThemeParsed` / `ColorPalette` / `Typography` / `BorderRadius` / `Shadow` / +`ThemeMode` types are removed from `@objectstack/spec` / `@objectstack/spec/ui` +(orphaned value schemas leave with their one consumer, #3950). `PUT +/api/v1/meta/theme/:name` now gets the #8421 unrecognised-type refusal — the +`themes: 'theme'` fold left `PLURAL_TO_SINGULAR` and with it the generated +URL-spelling contract — instead of the pre-#10194 store-anything branch. + +**What stays:** `app.branding.primaryColor` / `accentColor` — the one live +colour surface (objectui's `AppShell` reads it and derives `--primary`, +`--accent` and friends) — plus objectui's `ThemeEngine` / `ThemeContext` engine +code and their unit tests, explicitly retained by the ruling. Legacy stored +`theme` rows are untouched: reads still answer, DELETE still works, and +`applyConversionsToStoredItem` passes them through unchanged. + +The retirement kit: + +- strict deletion + `guidance` prescription at the stack schema + (`packages/spec/src/stack.zod.ts`); `packages/spec/src/ui/theme.zod.ts` + deleted whole +- ADR-0087 registration: retired-def entries `ui/Theme`, `ui/ThemeMode`, + `ui/ColorPalette`, `ui/Typography`, `ui/BorderRadius`, `ui/Shadow` and the + D3 **semantic** entry `stack-themes-carrier-retired` (protocol 18). Semantic + rather than a D2 conversion on the lossless-only scope guard: a stack may + declare N themes and M apps, so which palette entry becomes which app's + `branding.primaryColor` is a judgment the transform cannot make — the entry + prescribes the hand move instead of auto-deleting authored content +- ingest mapping removed (`packages/metadata/src/plugin.ts`), CLI stats row + removed, showcase example re-based on app branding +- pin tests: `stack-top-level-strict.test.ts` (refusal carries `#10485` + + `app.branding` + no rename suggestion; replacement parses green; no theme + export survives on `./ui`) and `protocol.unrecognised-meta-type.test.ts` + (`/meta/theme` refused with the ADR-0112 envelope, nothing stored) +- generated baselines/docs follow the schema (`authorable-surface/`, + `json-schema.manifest/`, api-surface, export-origins, meta-url-spelling, + spec-changes, upgrade guide, reference docs, skill references) + +## FROM → TO + +```ts +// before — parsed green, stored by artifact ingest, applied by NOTHING: +defineStack({ + themes: [{ name: 'corporate', label: 'Corporate', mode: 'light', + colors: { primary: '#7C3AED' } }], +}); + +// after — delete the key; colour the console where something reads it: +defineApp({ + name: 'my_app', + label: 'My App', + branding: { primaryColor: '#7C3AED', accentColor: '#06B6D4' }, +}); +// a custom CSS variable your own stylesheet consumed has no spec slot any +// more — move it into your own CSS. +``` + + diff --git a/content/docs/getting-started/examples.mdx b/content/docs/getting-started/examples.mdx index 47bcb0878d..032ff92250 100644 --- a/content/docs/getting-started/examples.mdx +++ b/content/docs/getting-started/examples.mdx @@ -277,7 +277,7 @@ os compile # Build to dist/objectstack.json A kitchen-sink workspace built for demonstration and debugging. It exercises nearly every metadata type, view type, and chart type in a single app — objects, views, apps, pages, dashboards, reports, datasets, flows, jobs, agents, security -profiles, translations, themes, webhooks, and more. Use it as a living reference +profiles, translations, webhooks, and more. Use it as a living reference when you want to see how a particular metadata type is authored. ```bash diff --git a/content/docs/getting-started/quick-reference.mdx b/content/docs/getting-started/quick-reference.mdx index d52e165136..d4873fe836 100644 --- a/content/docs/getting-started/quick-reference.mdx +++ b/content/docs/getting-started/quick-reference.mdx @@ -48,7 +48,7 @@ Core business logic and data modeling schemas. ## UI Protocol (11 of 17 schemas) -Presentation layer - views, forms, dashboards, and themes. +Presentation layer - views, forms, dashboards, and app branding. | Protocol | Source File | Key Schemas | Purpose | |:---------|:-----------|:------------|:--------| diff --git a/content/docs/getting-started/quick-start.mdx b/content/docs/getting-started/quick-start.mdx index c3e0ac60f8..16c09fbb9d 100644 --- a/content/docs/getting-started/quick-start.mdx +++ b/content/docs/getting-started/quick-start.mdx @@ -160,7 +160,7 @@ app-showcase/ ├── objectstack.config.ts ├── src/ │ ├── data/ # objects, extensions, hooks, mappings, analytics, seed -│ ├── ui/ # views, pages, apps, actions, dashboards, reports, datasets, themes +│ ├── ui/ # views, pages, apps, actions, dashboards, reports, datasets │ ├── automation/ # flows, jobs, webhooks │ ├── security/ # permission sets, positions, sharing rules, capabilities │ ├── system/ # apis, books, connectors, datasources, emails, server, translations @@ -203,7 +203,6 @@ is the full authorable set, ordered data → interface → automation → integr | `dashboards` | Chart and metric boards | [Dashboards](/docs/ui/dashboards) | | `reports` | Saved analytical queries with grouping and totals | [Reference](/docs/references/ui/report) | | `actions` | Buttons and bulk operations, with CEL visibility | [Actions](/docs/ui/actions) | -| `themes` | Color tokens and branding | [Reference](/docs/references/ui/theme) | | `translations` | i18n bundles for labels and messages | [Translations](/docs/ui/translations) | | `docs` | In-app Markdown documentation items | [Doc Pages](/docs/ui/doc-pages) | | `books` | Ordered navigation spines over those doc items | [Reference](/docs/references/system/book) | diff --git a/content/docs/protocol/objectui/widget-contract.mdx b/content/docs/protocol/objectui/widget-contract.mdx index c083846ffd..52feaec6f6 100644 --- a/content/docs/protocol/objectui/widget-contract.mdx +++ b/content/docs/protocol/objectui/widget-contract.mdx @@ -269,54 +269,29 @@ parse-clean no-op. Large datasets page via the view's `pagination` block. ## Theme -ObjectUI theming is defined by `ThemeSchema` in `packages/spec/src/ui/theme.zod.ts`, and it declares **ten** authorable keys — that list is the whole vocabulary. Five of them are identity and inheritance: `name` (a snake_case identifier) and `label` are required, `description` is optional, `mode` is one of `light`, `dark`, or `auto` (default `light`), and `extends` names another theme to inherit from. The other five are the token surface: - -| Key | Required | Shape | What it puts on the document | -|-------|-------|-------|-------| -| `colors` | ✅ | `ColorPalette`; only `primary` is mandatory inside it | The shadcn palette variables — **renamed on the way out**: `surface` emits `--card`, `text` emits `--foreground`, `textSecondary` emits `--muted-foreground`, `disabled` emits `--muted`, `error` emits `--destructive`. | -| `borderRadius` | — | A scale object (`none`/`sm`/`base`/`md`/`lg`/`xl`/`2xl`/`full`), not a single token | `--radius-sm`, `--radius-md`, ... — and `base` emits the bare `--radius`. | -| `shadows` | — | The same stops plus `inner` | `--shadow-sm`, `--shadow-md`, ... — and `base` emits the bare `--shadow`. | -| `typography` | — | One live key since #5021: `fontFamily.base` | `--font-sans`. | -| `customVars` | — | A flat string map | Every entry verbatim, `--` prefixed if you omit it: `z-modal: '1050'` emits `--z-modal: 1050`. This is the declared door for any other custom property. | - -```yaml -name: corporate -label: Corporate -mode: light -colors: - primary: '#2563eb' - surface: '#ffffff' - text: '#111827' -borderRadius: - base: 0.25rem - md: 0.375rem -shadows: - base: '0 1px 3px rgb(0 0 0 / 0.1)' -typography: - fontFamily: - base: 'Inter, system-ui, sans-serif' -customVars: - space-4: 1rem +The `themes` authoring surface was **retired** in `@objectstack/spec` 17.1 +(#10485, ADR-0049 enforce-or-remove). Authored themes were parsed and stored, +but no framework package ever read them back and nothing selected an active +theme, so a declared theme never changed anything on screen. A stack that still +declares `themes:` is now refused at parse with a prescription pointing here. + +**`app.branding` is the one colour surface.** Set +`branding.primaryColor` / `branding.accentColor` on the app +(`packages/spec/src/ui/app.zod.ts`): objectui's `AppShell` converts them to +HSL and writes `--primary`, `--primary-foreground`, `--ring`, +`--sidebar-primary`, `--sidebar-ring`, `--accent` and `--accent-foreground`, +re-deriving them on the light/dark flip. + +```ts +export const MyApp = defineApp({ + name: 'my_app', + label: 'My App', + branding: { primaryColor: '#2563eb', accentColor: '#06b6d4' }, +}); ``` -`ThemeSchema` is `.strict()` (#4001), so a key outside that list is a **parse failure** at `defineStack({ themes })` / `defineTheme()`, carrying its own prescription — not a value silently dropped while the theme still reports valid. - - - **Older theme samples no longer parse** — check yours before copying it forward. - **#3494** removed `spacing`, `breakpoints`, `logo`, `density`, `wcagContrast`, - `rtl`, `touchTarget` and `keyboardNavigation`: the theme engine never emitted a - variable for any of them, so authoring one was a silent no-op. **#5021** - (`@objectstack/spec` 17.0.0, ADR-0049) removed `animation`, `zIndex`, the - `typography.fontSize` / `fontWeight` / `lineHeight` / `letterSpacing` scales and - `typography.fontFamily.heading` / `mono`: those *were* emitted, faithfully and - for years, but no first-party component or stylesheet has ever read one. - The prescription in both waves is `customVars`, and it is a byte-for-byte - replacement — `customVars` carrying `font-size-lg: 1.125rem` puts exactly the - same `--font-size-lg` on the document the retired scale did. Run - `os migrate meta --from 16` to rewrite stored metadata automatically. - - -Widgets inherit the active theme automatically; they do not each carry their own copy of the palette or the font stack. +Widgets read the resulting CSS variables (`hsl(var(--primary))`, …); they do +not each carry their own copy of the palette. ## What's Next? diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 3dc3f1d836..0241c62d32 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1589 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1583 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -32,8 +32,8 @@ counts are sums of the rows they head. Regenerate with | [Shared Protocol](/docs/references/shared) | 8 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. | | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 36 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | -| [UI Protocol](/docs/references/ui) | 17 | 162 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **199** | **1589** | 14 protocol modules | +| [UI Protocol](/docs/references/ui) | 16 | 156 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | +| **Total** | **198** | **1583** | 14 protocol modules | --- @@ -364,7 +364,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a ## UI Protocol -**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **17 pages, 162 schemas** +**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 156 schemas** Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. @@ -385,7 +385,6 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay | [`report.zod.ts`](/docs/references/ui/report) | `JoinedReportBlock`, `Report`, `ReportChart`, `ReportSort`, `ReportType` | | [`responsive.zod.ts`](/docs/references/ui/responsive) | `BreakpointColumnMap`, `BreakpointName`, `BreakpointOrderMap`, `ResponsiveConfig`, `ResponsiveStyles`, `StyleMap` | | [`sharing.zod.ts`](/docs/references/ui/sharing) | `SharingConfig` | -| [`theme.zod.ts`](/docs/references/ui/theme) | `BorderRadius`, `ColorPalette`, `Shadow`, `Theme`, `ThemeMode`, `Typography` | | [`view.zod.ts`](/docs/references/ui/view) | `AddRecordConfig`, `AppearanceConfig`, `CalendarConfig`, `ColumnPrefix`, `ColumnSummary`, `ColumnSummaryConfig`, `FormButtonConfig`, `FormField`, `FormFieldPublicPicker`, `FormSection`, `FormView`, `GalleryConfig`, `GanttConfig`, `GanttQuickFilter`, `GroupingConfig`, `GroupingField`, `HttpMethodSubset`, `HttpRequest`, `KanbanConfig`, `ListChartConfig`, `ListColumn`, `ListMapConfig`, `ListView`, `NavigationConfig`, `NavigationMode`, `ObjectListView`, `ObjectUserFilters`, `PaginationConfig`, `RowColorConfig`, `RowHeight`, `SelectionConfig`, `TimelineConfig`, `TreeConfig`, `UserActionsConfig`, `UserFilterField`, `UserFilters`, `View`, `ViewData`, `ViewFilterRule`, `ViewItem`, `ViewItemName`, `ViewItemWire`, `ViewKind`, `ViewScope`, `ViewSharing`, `ViewTab`, `VisualizationType` | --- diff --git a/content/docs/references/ui/index.mdx b/content/docs/references/ui/index.mdx index 19cb0ebd07..7fee054ec5 100644 --- a/content/docs/references/ui/index.mdx +++ b/content/docs/references/ui/index.mdx @@ -21,6 +21,5 @@ This section contains all protocol schemas for the ui layer of ObjectStack. - diff --git a/content/docs/references/ui/meta.json b/content/docs/references/ui/meta.json index 5eedbea53a..761d57bf10 100644 --- a/content/docs/references/ui/meta.json +++ b/content/docs/references/ui/meta.json @@ -16,7 +16,6 @@ "report", "---Interaction & Layout---", "responsive", - "theme", "---Platform---", "i18n", "notification", diff --git a/content/docs/references/ui/theme.mdx b/content/docs/references/ui/theme.mdx deleted file mode 100644 index d8e46c344d..0000000000 --- a/content/docs/references/ui/theme.mdx +++ /dev/null @@ -1,144 +0,0 @@ ---- -title: Theme -description: Theme protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -Color Palette Schema -Defines brand colors and their variants. - - -**Source:** `packages/spec/src/ui/theme.zod.ts` - - -## TypeScript Usage - -```typescript -import { BorderRadiusSchema, ColorPaletteSchema, ShadowSchema, ThemeSchema, ThemeModeSchema, TypographySchema } from '@objectstack/spec/ui'; -import type { BorderRadius, ColorPalette, Shadow, Theme, ThemeMode, Typography } from '@objectstack/spec/ui'; - -// Validate data -const result = BorderRadiusSchema.parse(data); -``` - ---- - -## BorderRadius - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **none** | `string` | optional | No border radius (0) | -| **sm** | `string` | optional | Small border radius (e.g., 0.125rem) | -| **base** | `string` | optional | Base border radius (e.g., 0.25rem) | -| **md** | `string` | optional | Medium border radius (e.g., 0.375rem) | -| **lg** | `string` | optional | Large border radius (e.g., 0.5rem) | -| **xl** | `string` | optional | Extra large border radius (e.g., 0.75rem) | -| **2xl** | `string` | optional | 2X large border radius (e.g., 1rem) | -| **full** | `string` | optional | Full border radius (50%) | - - ---- - -## ColorPalette - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **primary** | `string` | ✅ | Primary brand color (hex, rgb, or hsl) | -| **secondary** | `string` | optional | Secondary brand color | -| **accent** | `string` | optional | Accent color for highlights | -| **success** | `string` | optional | Success state color (default: green) | -| **warning** | `string` | optional | Warning state color (default: yellow) | -| **error** | `string` | optional | Error state color (default: red) | -| **info** | `string` | optional | Info state color (default: blue) | -| **background** | `string` | optional | Background color | -| **surface** | `string` | optional | Surface/card background color | -| **text** | `string` | optional | Primary text color | -| **textSecondary** | `string` | optional | Secondary text color | -| **border** | `string` | optional | Border color | -| **disabled** | `string` | optional | Disabled state color | -| **primaryLight** | `string` | optional | Lighter shade of primary | -| **primaryDark** | `string` | optional | Darker shade of primary | -| **secondaryLight** | `string` | optional | Lighter shade of secondary | -| **secondaryDark** | `string` | optional | Darker shade of secondary | - - ---- - -## Shadow - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **none** | `string` | optional | No shadow | -| **sm** | `string` | optional | Small shadow | -| **base** | `string` | optional | Base shadow | -| **md** | `string` | optional | Medium shadow | -| **lg** | `string` | optional | Large shadow | -| **xl** | `string` | optional | Extra large shadow | -| **2xl** | `string` | optional | 2X large shadow | -| **inner** | `string` | optional | Inner shadow (inset) | - - ---- - -## Theme - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Unique theme identifier (snake_case) | -| **label** | `string` | ✅ | Human-readable theme name | -| **description** | `string` | optional | Theme description | -| **mode** | `Enum<'light' \| 'dark' \| 'auto'>` | optional (default: `"light"`) | Theme mode (light, dark, or auto) | -| **colors** | `{ primary: string; secondary?: string; accent?: string; success?: string; … }` | ✅ | Color palette configuration | -| **typography** | `{ fontFamily?: object }` | optional | Typography settings | -| **borderRadius** | `{ none?: string; sm?: string; base?: string; md?: string; … }` | optional | Border radius scale | -| **shadows** | `{ none?: string; sm?: string; base?: string; md?: string; … }` | optional | Box shadow effects | -| **animation** | `never` | optional | [REMOVED] `theme.animation` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — unlike the #3494 props above, the engine DID emit `--duration-*` and `--timing-*`, faithfully and for years; what never existed was a reader. No first-party component or stylesheet has ever consumed one, so every transition ran at the renderer default whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "duration-fast": "150ms", "timing-ease_in": "cubic-bezier(0.4, 0, 1, 1)" }` emits exactly the same properties). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **zIndex** | `never` | optional | [REMOVED] `theme.zIndex` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--z-base` … `--z-tooltip` and nothing read one, so an overlay you "lifted" still stacked by document order. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "z-modal": "1050" }` emits exactly the same `--z-modal`). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **customVars** | `Record` | optional | Custom CSS variables (key-value pairs) | -| **extends** | `string` | optional | Base theme to extend from | -| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | -| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | -| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | -| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). | -| **_packageId** | `string` | optional | Owning package machine id. | -| **_packageVersion** | `string` | optional | Owning package version. | -| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | - - ---- - -## ThemeMode - -### Allowed Values - -* `light` -* `dark` -* `auto` - - ---- - -## Typography - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **fontFamily** | `{ base?: string }` | optional | | -| **fontSize** | `never` | optional | [REMOVED] `theme.typography.fontSize` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-size-xs` … `--font-size-4xl` faithfully and NO first-party component or stylesheet has ever read one, so a declared type scale was real CSS that styled nothing. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-size-lg": "1.125rem" }` emits exactly the same `--font-size-lg`). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **fontWeight** | `never` | optional | [REMOVED] `theme.typography.fontWeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-weight-*` and nothing read it, so text rendered at the inherited weight whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-weight-semibold": "600" }` emits exactly the same `--font-weight-semibold`). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **lineHeight** | `never` | optional | [REMOVED] `theme.typography.lineHeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--line-height-*` and nothing read it, so every block kept its inherited leading. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "line-height-relaxed": "1.75" }` emits exactly the same `--line-height-relaxed`). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **letterSpacing** | `never` | optional | [REMOVED] `theme.typography.letterSpacing` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--letter-spacing-*` and nothing read it, so tracking never moved. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "letter-spacing-wide": "0.025em" }` emits exactly the same `--letter-spacing-wide`). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | - - ---- - diff --git a/content/docs/ui/index.mdx b/content/docs/ui/index.mdx index 6cc32d9717..769d1128c7 100644 --- a/content/docs/ui/index.mdx +++ b/content/docs/ui/index.mdx @@ -1,6 +1,6 @@ --- title: UI Engine -description: Apps, views, dashboards, themes, and public forms — server-driven UI declared as metadata and rendered by the ObjectUI runtime. +description: Apps, views, dashboards, branding, and public forms — server-driven UI declared as metadata and rendered by the ObjectUI runtime. --- # UI Engine @@ -32,7 +32,7 @@ export const CrmApp = App.create({ - **Apps** group navigation, branding, and entry points for one audience. - **Views** present object records as `grid` (the standard data table), `kanban`, `gallery`, `calendar`, `timeline`, `gantt`, `map`, `chart`, or `tree`. Forms are their own view kind with per-mode layouts ([Views](/docs/ui/views)). - **Pages** compose free-form layouts from widgets; **Dashboards** combine charts, reports, and datasets for analytics. -- **Themes** define palettes, typography, and spacing as metadata — the showcase example ships light and dark theme variants. +- **Branding** colours an app: `app.branding.primaryColor` / `accentColor` drive the console's `--primary` / `--accent` variables. (The standalone `themes` collection was retired in spec 17.1, ADR-0049 — it was parsed but never applied.) - **Public forms** expose a scoped slice of an object to external audiences through **anonymous entry routes** for public data collection ([Forms](/docs/ui/forms), [public data collection](/docs/ui/public-data-collection)). - The **Setup App** — the platform's built-in administration UI — is itself rendered from the same protocol ([Setup App](/docs/ui/setup-app)). diff --git a/content/docs/ui/react-pages.mdx b/content/docs/ui/react-pages.mdx index 7f60c43fe7..11f42ab61c 100644 --- a/content/docs/ui/react-pages.mdx +++ b/content/docs/ui/react-pages.mdx @@ -182,8 +182,10 @@ a modal's `bg-black/50` backdrop rendered fully transparent in production. Style a react page two ways instead: **1. Layout and chrome — inline `style={{ … }}` with theme tokens.** Colors come from the -active theme as `hsl(var(--token))`, so the page follows light/dark and any theme the -deployment installs: +base stylesheet's shadcn token set as `hsl(var(--token))` — the values are bare HSL +triples, and `app.branding.primaryColor` / `accentColor` re-derive `--primary` / +`--accent` on the light/dark flip — so the page follows light/dark and the app's +branding: ```jsx
= { { name: 'description', type: 'string', description: 'App description' }, { name: 'navigation', type: 'NavItem[]', description: 'Menu tree structure' }, { name: 'logo', type: 'string', description: 'Logo URL or asset path' }, - { name: 'theme', type: 'string', description: 'Theme reference' }, + // `theme` here described a key that is only an alias of `branding`; the + // `themes` metadata surface itself was retired at #10485 (ADR-0049) — + // `app.branding` is the one colour surface. + { name: 'branding', type: 'AppBranding', description: 'Brand colors and logo (primaryColor, accentColor, logo)' }, { name: 'defaultRoute', type: 'string', description: 'Landing page route' }, ], example: `{ diff --git a/packages/cli/src/utils/format.ts b/packages/cli/src/utils/format.ts index f2eae30076..4be479ce67 100644 --- a/packages/cli/src/utils/format.ts +++ b/packages/cli/src/utils/format.ts @@ -346,7 +346,6 @@ export interface MetadataStats { apis: number; positions: number; permissions: number; - themes: number; datasources: number; translations: number; plugins: number; @@ -386,7 +385,6 @@ export function collectMetadataStats(config: any): MetadataStats { apis: count(config.apis), positions: count(config.positions), permissions: count(config.permissions), - themes: count(config.themes), datasources: count(config.datasources), translations: count(config.translations), plugins: count(config.plugins), @@ -771,7 +769,6 @@ export function printMetadataStats(stats: MetadataStats) { ['Dashboards', stats.dashboards], ['Reports', stats.reports], ['Actions', stats.actions], - ['Themes', stats.themes], ], }, { diff --git a/packages/metadata-protocol/src/protocol.meta-types-mint-door-agreement.test.ts b/packages/metadata-protocol/src/protocol.meta-types-mint-door-agreement.test.ts index 6013ee70cb..a087ed74ad 100644 --- a/packages/metadata-protocol/src/protocol.meta-types-mint-door-agreement.test.ts +++ b/packages/metadata-protocol/src/protocol.meta-types-mint-door-agreement.test.ts @@ -30,21 +30,22 @@ * ## Why the sample spans THREE classes and not just the withdrawn four * * A suite that only pinned the four withdrawn types would be satisfied by a - * blanket flip of the synthesis — which would break `PUT /meta/theme/dark`, the - * operation the plugin path exists to serve, and would be a worse outcome than - * the defect being closed. So every case here carries its class, and the + * blanket flip of the synthesis — which would break `PUT /meta/webhook/stripe`, + * the operation the plugin path exists to serve, and would be a worse outcome + * than the defect being closed. So every case here carries its class, and the * classes are checked against each other: * * 1. **statically declared** (`view`, `hook`, `agent`) — the flag comes off * the registry entry, as it always did, in both the `true` and the `false` * direction; - * 2. **URL-map-only plugin kinds** (`theme`, and its five siblings) — no + * 2. **URL-map-only plugin kinds** (`webhook`, and its four siblings) — no * registry entry, IN the static spelling contract, still advertised and * still mintable. This is the discriminating control: without it the change * cannot show its narrowing is narrow; - * 3. **withdrawn** (`policy`, `data`, `package`, `kind`) — live - * `SchemaRegistry` keys an ordinary `registerApp` produces, in NEITHER half - * of the static contract, advertised `false` and refused. + * 3. **withdrawn** (`policy`, `data`, `package`, `kind` — and, since #10485, + * `theme`, whose carrier retired out of the spelling contract while legacy + * stored rows can still hold the key live) — live `SchemaRegistry` keys, + * in NEITHER half of the static contract, advertised `false` and refused. * * Harness: the real `getMetaTypes()` and the real `saveMetaItem()` on one * protocol instance over a stub engine, so agreement is MEASURED across the two @@ -152,13 +153,24 @@ const SAMPLE: Array<{ item: { name: 'probe_agent', label: 'Probe' }, }, { - type: 'theme', + type: 'webhook', klass: 'url-map-only', creatable: true, - // [#10194] spec-valid body — `theme` resolves a schema through - // UNREGISTERED_KIND_SCHEMAS now, and the "behaves as advertised" case + // [#6245] spec-valid body — `webhook` resolves a schema through + // UNREGISTERED_KIND_SCHEMAS, and the "behaves as advertised" case // drives this body through a real write, so a malformed one would // 422 and misread the ADVERTISEMENT door this suite measures. + // (`theme` held this slot until #10485 retired the themes surface.) + item: { name: 'probe_webhook', label: 'Probe', object: 'task', triggers: ['create'], url: 'https://example.com/hook' }, + }, + { + // [#10485] `theme` moved from class 2 to class 3: the carrier retired + // out of the spelling contract, while a legacy environment's stored + // rows can still hold the key in the live set — so it must be + // advertised `false` and refused at the mint door, like the four. + type: 'theme', + klass: 'withdrawn', + creatable: false, item: { name: 'probe_theme', label: 'Probe', colors: { primary: '#3b82f6' } }, }, { type: 'policy', klass: 'withdrawn', creatable: false, item: { name: 'probe_policy', label: 'Probe' } }, @@ -231,17 +243,17 @@ describe('#8421 — the read door and the mint door agree, across all three clas } }); - it('the six URL-map-only plugin kinds are ALL still advertised as creatable', async () => { - // The blanket-flip guard, quantified rather than sampled. `theme` above - // is the one driven end-to-end through a write; these five have no - // hand-written spec-valid body here, so they are pinned on the door + it('the five URL-map-only plugin kinds are ALL still advertised as creatable', async () => { + // The blanket-flip guard, quantified rather than sampled. `webhook` + // above is the one driven end-to-end through a write; the others have + // no hand-written spec-valid body here, so they are pinned on the door // that this change actually moved — the advertisement. Breaking any of // them is the one outcome that would make this change worse than the - // defect it closes. + // defect it closes. (`theme` left the set at #10485.) const { protocol } = makeProtocol(); const listing = await protocol.getMetaTypes(); for (const kind of [ - 'analytics_cube', 'connector', 'rag_pipeline', 'sharing_rule', 'theme', 'webhook', + 'analytics_cube', 'connector', 'rag_pipeline', 'sharing_rule', 'webhook', ]) { const entry = listing.entries.find((e: any) => e.type === kind); expect(entry, `${kind} must be listed`).toBeDefined(); diff --git a/packages/metadata-protocol/src/protocol.unrecognised-meta-type.test.ts b/packages/metadata-protocol/src/protocol.unrecognised-meta-type.test.ts index 1930d0b8c4..1f1d36ae72 100644 --- a/packages/metadata-protocol/src/protocol.unrecognised-meta-type.test.ts +++ b/packages/metadata-protocol/src/protocol.unrecognised-meta-type.test.ts @@ -27,7 +27,8 @@ * * - a DECLARED type still saves (`view`), and so does a type whose only write * channel is runtime (`hook`); - * - a PLUGIN kind with no static registry entry still saves (`theme`) — the + * - a PLUGIN kind with no static registry entry still saves (`webhook`; + * `theme` was the specimen until #10485 retired that kind entirely) — the * operation option C would have broken, and the one this change must not; * - READS of an unrecognised type still answer, because the live type set * legitimately holds keys the static contract does not (`data`, `kind` and @@ -163,6 +164,18 @@ describe('#8421 — an unrecognised `/meta` type is refused instead of minted', expect(metaRows(rows).length).toBe(0); }); + it("[#10485] `theme` is now on the refused side — the retired kind left the spelling contract", async () => { + // Until #10485, `theme` was a URL-map-only plugin kind and this suite's + // ACCEPTED specimen. The retirement removed the `themes: 'theme'` fold + // from `PLURAL_TO_SINGULAR`, so `/meta/theme` now earns the same + // ADR-0112 refusal as any minted namespace — loud, and nothing stored. + const { protocol, rows } = makeProtocol(); + await expect( + protocol.saveMetaItem({ type: 'theme', name: 'dark', item: { name: 'dark', label: 'Dark', colors: { primary: '#3b82f6' } } }), + ).rejects.toMatchObject({ code: 'INVALID_REQUEST', status: 400 }); + expect(metaRows(rows).length).toBe(0); + }); + it('names the offending type and why, per the 2026-08-12 refusal ruling', async () => { const { protocol } = makeProtocol(); await expect( @@ -201,10 +214,12 @@ describe('#8421 — the traffic that must keep working', () => { item: { name: 'probe_item', object: 'task', events: ['beforeUpdate'] }, }, { - type: 'theme', + // `theme` held this slot until #10485 retired the themes surface + // (ADR-0049) and `theme` left the URL-spelling contract with it. + type: 'webhook', why: 'PLUGIN kind — no static registry entry at all', - // [#10194] spec-valid body — theme resolves a schema now. - item: { name: 'probe_item', label: 'Probe', colors: { primary: '#3b82f6' } }, + // [#6245] spec-valid body — webhook resolves a schema. + item: { name: 'probe_item', label: 'Probe', object: 'task', triggers: ['create'], url: 'https://example.com/hook' }, }, ]; @@ -220,20 +235,20 @@ describe('#8421 — the traffic that must keep working', () => { it('POSITIVE CONTROL — the plugin path still serves its first create', async () => { // The measurement that disqualified option C, kept as a live control: - // `theme` has ZERO items at this moment, which is exactly the state a + // `webhook` has ZERO items at this moment, which is exactly the state a // live-registry check would have refused. The refusal that shipped // consults the static contract instead, so the first create of a // plugin kind is untouched. const { protocol, rows } = makeProtocol(); - // [#10194] spec-valid body — theme resolves a schema now, and this - // control measures the STATIC-contract door, not the shape check. + // Spec-valid body — this control measures the STATIC-contract door, + // not the shape check. (`theme` was the specimen until #10485.) const result = await protocol.saveMetaItem({ - type: 'theme', - name: 'dark', - item: { name: 'dark', label: 'Dark', colors: { primary: '#3b82f6' } }, + type: 'webhook', + name: 'first_hook', + item: { name: 'first_hook', label: 'First', object: 'task', triggers: ['create'], url: 'https://example.com/hook' }, }); expect(result.success).toBe(true); - expect(metaRows(rows)[0]!.type).toBe('theme'); + expect(metaRows(rows)[0]!.type).toBe('webhook'); }); }); diff --git a/packages/metadata/src/plugin.ts b/packages/metadata/src/plugin.ts index 2dc31f9e78..dd7f5062e9 100644 --- a/packages/metadata/src/plugin.ts +++ b/packages/metadata/src/plugin.ts @@ -73,7 +73,10 @@ const ARTIFACT_FIELD_TO_TYPE: Record = { dashboards: 'dashboard', reports: 'report', actions: 'action', - themes: 'theme', + // `themes: 'theme'` removed at #10485 (ADR-0049): the carrier key is + // retired, so a parsed artifact can no longer carry the field — and the + // ingest half of the dead pipeline (items stored, read by nothing) goes + // with the authoring half rather than surviving it as drift. workflows: 'workflow', flows: 'flow', // ADR-0090 D3: stacks declare `positions` (stack.zod.ts); the retired diff --git a/packages/qa/downstream-contract/src/additional-domains.fixtures.ts b/packages/qa/downstream-contract/src/additional-domains.fixtures.ts index fe312bca7d..2d46f2037b 100644 --- a/packages/qa/downstream-contract/src/additional-domains.fixtures.ts +++ b/packages/qa/downstream-contract/src/additional-domains.fixtures.ts @@ -20,7 +20,6 @@ import type { SharingRule, PermissionSet } from '@objectstack/spec/security'; import type { Position } from '@objectstack/spec/identity'; import type { EmailTemplateDefinition, TranslationBundle } from '@objectstack/spec/system'; import type { Webhook } from '@objectstack/spec/automation'; -import type { Theme } from '@objectstack/spec/ui'; export const DcDatasource: Datasource = { name: 'dc_primary', @@ -131,18 +130,9 @@ export const DcMapping: Mapping = { fieldMapping: [{ source: 'Name', target: 'name', transform: 'none' }], }; -export const DcTheme: Theme = { - name: 'dc_light', - label: 'DC Light', - mode: 'light', - colors: { - primary: '#1E6FD9', - secondary: '#6C757D', - background: '#FFFFFF', - surface: '#F8F9FA', - text: '#212529', - }, -}; +// `DcTheme` left with `ThemeSchema` (#10485, ADR-0049 — the theme authoring +// surface is retired; the freeze pins author state against a LIVE surface, and +// this one no longer exists). export const DcTranslationBundle: TranslationBundle = { en: { diff --git a/packages/qa/downstream-contract/test/contract.test.ts b/packages/qa/downstream-contract/test/contract.test.ts index 7e89b1a255..e1ccf5ff67 100644 --- a/packages/qa/downstream-contract/test/contract.test.ts +++ b/packages/qa/downstream-contract/test/contract.test.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { describe, it, expect } from 'vitest'; -import { ActionSchema, ReportSchema, PageSchema, ThemeSchema } from '@objectstack/spec/ui'; +import { ActionSchema, ReportSchema, PageSchema } from '@objectstack/spec/ui'; import { DatasourceSchema, MappingSchema, CubeSchema, ObjectExtensionSchema } from '@objectstack/spec/data'; import { ConnectorSchema } from '@objectstack/spec/integration'; import { SharingRuleSchema, PermissionSetSchema } from '@objectstack/spec/security'; @@ -43,7 +43,7 @@ describe('downstream consumer contract (#2035)', () => { ['ObjectExtension', ObjectExtensionSchema, more.DcObjectExtension], ['Cube', CubeSchema, more.DcCube], ['Mapping', MappingSchema, more.DcMapping], - ['Theme', ThemeSchema, more.DcTheme], + // ['Theme', …] left with ThemeSchema (#10485, ADR-0049). ['TranslationBundle', TranslationBundleSchema, more.DcTranslationBundle], ]; diff --git a/packages/spec/api-surface-signatures.json b/packages/spec/api-surface-signatures.json index 61cbb5119e..b2099d1182 100644 --- a/packages/spec/api-surface-signatures.json +++ b/packages/spec/api-surface-signatures.json @@ -21,7 +21,6 @@ "defineSharingRule": "sha256:239e6649b55712a4", "defineSkill": "sha256:b928cee8cb3c2861", "defineStack": "sha256:4d36d9603c011c44", - "defineTheme": "sha256:21563e61ca2209e8", "defineTool": "sha256:47ab5254a14f1cf5", "defineTranslationBundle": "sha256:2716798bbd575af2", "defineView": "sha256:00cf9d0d2f44e843", diff --git a/packages/spec/api-surface/root.json b/packages/spec/api-surface/root.json index a32100f87b..3d0bbf5610 100644 --- a/packages/spec/api-surface/root.json +++ b/packages/spec/api-surface/root.json @@ -155,7 +155,6 @@ "defineSharingRule (function)", "defineSkill (function)", "defineStack (function)", - "defineTheme (function)", "defineTool (function)", "defineTranslationBundle (function)", "defineView (function)", diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index 972e7560f4..73a48b4051 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -47,8 +47,6 @@ "AssembledViewArtifactParsed (type)", "AssembledViewArtifactSchema (const)", "AssembledViewPartition (interface)", - "BorderRadius (type)", - "BorderRadiusSchema (const)", "BreakpointColumnMap (type)", "BreakpointColumnMapSchema (const)", "BreakpointName (type)", @@ -92,8 +90,6 @@ "ChartSeriesSchema (const)", "ChartType (type)", "ChartTypeSchema (const)", - "ColorPalette (type)", - "ColorPaletteSchema (const)", "ColumnPrefix (type)", "ColumnPrefixParsed (type)", "ColumnPrefixSchema (const)", @@ -331,25 +327,16 @@ "SelectionConfig (type)", "SelectionConfigParsed (type)", "SelectionConfigSchema (const)", - "Shadow (type)", - "ShadowSchema (const)", "SharingConfig (type)", "SharingConfigParsed (type)", "SharingConfigSchema (const)", "StyleMap (type)", "StyleMapSchema (const)", - "Theme (type)", - "ThemeMode (type)", - "ThemeModeSchema (const)", - "ThemeParsed (type)", - "ThemeSchema (const)", "TimelineConfig (type)", "TimelineConfigParsed (type)", "TimelineConfigSchema (const)", "TreeConfig (type)", "TreeConfigSchema (const)", - "Typography (type)", - "TypographySchema (const)", "UrlNavItem (type)", "UrlNavItemParsed (type)", "UrlNavItemSchema (const)", @@ -420,7 +407,6 @@ "defineForm (function)", "definePage (function)", "defineReport (function)", - "defineTheme (function)", "defineView (function)", "defineViewItem (function)", "diagnoseViewMetadata (function)", diff --git a/packages/spec/authorable-defaults/ui.json b/packages/spec/authorable-defaults/ui.json index 3945f9b364..6a7edd4d6b 100644 --- a/packages/spec/authorable-defaults/ui.json +++ b/packages/spec/authorable-defaults/ui.json @@ -105,7 +105,6 @@ "ui/SelectionConfig:type = \"none\"", "ui/SharingConfig:allowAnonymous = false", "ui/SharingConfig:enabled = false", - "ui/Theme:mode = \"light\"", "ui/TimelineConfig:scale = \"week\"", "ui/UrlNavItem:target = \"_self\"", "ui/UserActionsConfig:addRecordForm = false", diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index 8333f9d866..57d99fd848 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -139,14 +139,6 @@ "ui/AriaProps:ariaDescribedBy", "ui/AriaProps:ariaLabel", "ui/AriaProps:role", - "ui/BorderRadius:2xl", - "ui/BorderRadius:base", - "ui/BorderRadius:full", - "ui/BorderRadius:lg", - "ui/BorderRadius:md", - "ui/BorderRadius:none", - "ui/BorderRadius:sm", - "ui/BorderRadius:xl", "ui/BreakpointColumnMap:2xl", "ui/BreakpointColumnMap:lg", "ui/BreakpointColumnMap:md", @@ -238,23 +230,6 @@ "ui/ChartSeries:type", "ui/ChartSeries:variant", "ui/ChartSeries:yAxis", - "ui/ColorPalette:accent", - "ui/ColorPalette:background", - "ui/ColorPalette:border", - "ui/ColorPalette:disabled", - "ui/ColorPalette:error", - "ui/ColorPalette:info", - "ui/ColorPalette:primary", - "ui/ColorPalette:primaryDark", - "ui/ColorPalette:primaryLight", - "ui/ColorPalette:secondary", - "ui/ColorPalette:secondaryDark", - "ui/ColorPalette:secondaryLight", - "ui/ColorPalette:success", - "ui/ColorPalette:surface", - "ui/ColorPalette:text", - "ui/ColorPalette:textSecondary", - "ui/ColorPalette:warning", "ui/ColumnPrefix:field", "ui/ColumnPrefix:type", "ui/ColumnSummaryConfig:field", @@ -1118,39 +1093,12 @@ "ui/RowColorConfig:colors", "ui/RowColorConfig:field", "ui/SelectionConfig:type", - "ui/Shadow:2xl", - "ui/Shadow:base", - "ui/Shadow:inner", - "ui/Shadow:lg", - "ui/Shadow:md", - "ui/Shadow:none", - "ui/Shadow:sm", - "ui/Shadow:xl", "ui/SharingConfig:allowAnonymous", "ui/SharingConfig:allowedDomains", "ui/SharingConfig:enabled", "ui/SharingConfig:expiresAt", "ui/SharingConfig:password", "ui/SharingConfig:publicLink", - "ui/Theme:_lock", - "ui/Theme:_lockDocsUrl", - "ui/Theme:_lockReason", - "ui/Theme:_lockSource", - "ui/Theme:_packageId", - "ui/Theme:_packageVersion", - "ui/Theme:_provenance", - "ui/Theme:animation [RETIRED]", - "ui/Theme:borderRadius", - "ui/Theme:colors", - "ui/Theme:customVars", - "ui/Theme:description", - "ui/Theme:extends", - "ui/Theme:label", - "ui/Theme:mode", - "ui/Theme:name", - "ui/Theme:shadows", - "ui/Theme:typography", - "ui/Theme:zIndex [RETIRED]", "ui/TimelineConfig:colorField", "ui/TimelineConfig:endDateField", "ui/TimelineConfig:groupByField", @@ -1161,11 +1109,6 @@ "ui/TreeConfig:fields", "ui/TreeConfig:labelField", "ui/TreeConfig:parentField", - "ui/Typography:fontFamily", - "ui/Typography:fontSize [RETIRED]", - "ui/Typography:fontWeight [RETIRED]", - "ui/Typography:letterSpacing [RETIRED]", - "ui/Typography:lineHeight [RETIRED]", "ui/UrlNavItem:badge", "ui/UrlNavItem:badgeVariant", "ui/UrlNavItem:icon", diff --git a/packages/spec/export-origins/root.json b/packages/spec/export-origins/root.json index f256024404..6699a2fd4a 100644 --- a/packages/spec/export-origins/root.json +++ b/packages/spec/export-origins/root.json @@ -155,7 +155,6 @@ "defineSharingRule": "src/security/sharing.zod.ts#defineSharingRule (function)", "defineSkill": "src/ai/skill.zod.ts#defineSkill (function)", "defineStack": "src/stack.zod.ts#defineStack (function)", - "defineTheme": "src/ui/theme.zod.ts#defineTheme (function)", "defineTool": "src/ai/tool.zod.ts#defineTool (function)", "defineTranslationBundle": "src/system/translation.zod.ts#defineTranslationBundle (function)", "defineView": "src/ui/view.zod.ts#defineView (function)", diff --git a/packages/spec/export-origins/ui.json b/packages/spec/export-origins/ui.json index 23c74a8f86..df6d7b3150 100644 --- a/packages/spec/export-origins/ui.json +++ b/packages/spec/export-origins/ui.json @@ -47,8 +47,6 @@ "AssembledViewArtifactParsed": "src/ui/assembled-views.zod.ts#AssembledViewArtifactParsed (type)", "AssembledViewArtifactSchema": "src/ui/assembled-views.zod.ts#AssembledViewArtifactSchema (const)", "AssembledViewPartition": "src/ui/assembled-views.zod.ts#AssembledViewPartition (interface)", - "BorderRadius": "src/ui/theme.zod.ts#BorderRadius (type)", - "BorderRadiusSchema": "src/ui/theme.zod.ts#BorderRadiusSchema (const)", "BreakpointColumnMap": "src/ui/responsive.zod.ts#BreakpointColumnMap (type)", "BreakpointColumnMapSchema": "src/ui/responsive.zod.ts#BreakpointColumnMapSchema (const)", "BreakpointName": "src/ui/responsive.zod.ts#BreakpointName (type)", @@ -92,8 +90,6 @@ "ChartSeriesSchema": "src/ui/chart.zod.ts#ChartSeriesSchema (const)", "ChartType": "src/ui/chart.zod.ts#ChartType (type)", "ChartTypeSchema": "src/ui/chart.zod.ts#ChartTypeSchema (const)", - "ColorPalette": "src/ui/theme.zod.ts#ColorPalette (type)", - "ColorPaletteSchema": "src/ui/theme.zod.ts#ColorPaletteSchema (const)", "ColumnPrefix": "src/ui/view.zod.ts#ColumnPrefix (type)", "ColumnPrefixParsed": "src/ui/view.zod.ts#ColumnPrefixParsed (type)", "ColumnPrefixSchema": "src/ui/view.zod.ts#ColumnPrefixSchema (const)", @@ -331,25 +327,16 @@ "SelectionConfig": "src/ui/view.zod.ts#SelectionConfig (type)", "SelectionConfigParsed": "src/ui/view.zod.ts#SelectionConfigParsed (type)", "SelectionConfigSchema": "src/ui/view.zod.ts#SelectionConfigSchema (const)", - "Shadow": "src/ui/theme.zod.ts#Shadow (type)", - "ShadowSchema": "src/ui/theme.zod.ts#ShadowSchema (const)", "SharingConfig": "src/ui/sharing.zod.ts#SharingConfig (type)", "SharingConfigParsed": "src/ui/sharing.zod.ts#SharingConfigParsed (type)", "SharingConfigSchema": "src/ui/sharing.zod.ts#SharingConfigSchema (const)", "StyleMap": "src/ui/responsive.zod.ts#StyleMap (type)", "StyleMapSchema": "src/ui/responsive.zod.ts#StyleMapSchema (const)", - "Theme": "src/ui/theme.zod.ts#Theme (type)", - "ThemeMode": "src/ui/theme.zod.ts#ThemeMode (type)", - "ThemeModeSchema": "src/ui/theme.zod.ts#ThemeModeSchema (const)", - "ThemeParsed": "src/ui/theme.zod.ts#ThemeParsed (type)", - "ThemeSchema": "src/ui/theme.zod.ts#ThemeSchema (const)", "TimelineConfig": "src/ui/view.zod.ts#TimelineConfig (type)", "TimelineConfigParsed": "src/ui/view.zod.ts#TimelineConfigParsed (type)", "TimelineConfigSchema": "src/ui/view.zod.ts#TimelineConfigSchema (const)", "TreeConfig": "src/ui/view.zod.ts#TreeConfig (type)", "TreeConfigSchema": "src/ui/view.zod.ts#TreeConfigSchema (const)", - "Typography": "src/ui/theme.zod.ts#Typography (type)", - "TypographySchema": "src/ui/theme.zod.ts#TypographySchema (const)", "UrlNavItem": "src/ui/app.zod.ts#UrlNavItem (type)", "UrlNavItemParsed": "src/ui/app.zod.ts#UrlNavItemParsed (type)", "UrlNavItemSchema": "src/ui/app.zod.ts#UrlNavItemSchema (const)", @@ -420,7 +407,6 @@ "defineForm": "src/ui/view.zod.ts#defineForm (function)", "definePage": "src/ui/page.zod.ts#definePage (function)", "defineReport": "src/ui/report.zod.ts#defineReport (function)", - "defineTheme": "src/ui/theme.zod.ts#defineTheme (function)", "defineView": "src/ui/view.zod.ts#defineView (function)", "defineViewItem": "src/ui/view.zod.ts#defineViewItem (function)", "diagnoseViewMetadata": "src/ui/view.zod.ts#diagnoseViewMetadata (function)", diff --git a/packages/spec/json-schema.manifest/ui.json b/packages/spec/json-schema.manifest/ui.json index eb2b124e4e..6174a2db03 100644 --- a/packages/spec/json-schema.manifest/ui.json +++ b/packages/spec/json-schema.manifest/ui.json @@ -16,7 +16,6 @@ "ui/AppContextSelector", "ui/AppearanceConfig", "ui/AriaProps", - "ui/BorderRadius", "ui/BreakpointColumnMap", "ui/BreakpointName", "ui/BreakpointOrderMap", @@ -35,7 +34,6 @@ "ui/ChartInteraction", "ui/ChartSeries", "ui/ChartType", - "ui/ColorPalette", "ui/ColumnPrefix", "ui/ColumnSummary", "ui/ColumnSummaryConfig", @@ -139,14 +137,10 @@ "ui/RowColorConfig", "ui/RowHeight", "ui/SelectionConfig", - "ui/Shadow", "ui/SharingConfig", "ui/StyleMap", - "ui/Theme", - "ui/ThemeMode", "ui/TimelineConfig", "ui/TreeConfig", - "ui/Typography", "ui/UrlNavItem", "ui/UserActionsConfig", "ui/UserFilterField", diff --git a/packages/spec/scripts/build-skill-references.ts b/packages/spec/scripts/build-skill-references.ts index 429ca9a9f1..60d37b8c66 100644 --- a/packages/spec/scripts/build-skill-references.ts +++ b/packages/spec/scripts/build-skill-references.ts @@ -97,7 +97,7 @@ const SKILL_MAP: Record = { 'ui/widget.zod.ts', 'ui/component.zod.ts', 'ui/report.zod.ts', - 'ui/theme.zod.ts', + // 'ui/theme.zod.ts' — retired whole at #10485 (ADR-0049). 'ui/dataset.zod.ts', ], 'objectstack-platform': [ diff --git a/packages/spec/src/automation/sync-retirement.test.ts b/packages/spec/src/automation/sync-retirement.test.ts index 62442fff2f..65bba90f4a 100644 --- a/packages/spec/src/automation/sync-retirement.test.ts +++ b/packages/spec/src/automation/sync-retirement.test.ts @@ -204,7 +204,8 @@ describe('[#4738] sync/conflict dual-source retirement', () => { } // Anti-vacuity: the ui namespace we just probed is real and non-trivial — // otherwise a broken import would satisfy the three absences above. - expect('ThemeSchema' in ui).toBe(true); + // (`ThemeSchema` was the probe until #10485 retired it — ADR-0049.) + expect('PageSchema' in ui).toBe(true); }); it('still parses authored connector syncConfig through the renamed enum — the live path', async () => { diff --git a/packages/spec/src/index.ts b/packages/spec/src/index.ts index fdec3e9fc5..251b965fa5 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -145,7 +145,8 @@ export type { } from './data/authoring-key-lint'; export { defineCube } from './data/analytics.zod'; export { defineMapping } from './data/mapping.zod'; -export { defineTheme } from './ui/theme.zod'; +// `defineTheme` was removed at #10485 with `ui/theme.zod.ts` (ADR-0049) — see +// the block in `./ui/index.ts`; `app.branding` is the one colour surface. export { defineTranslationBundle } from './system/translation.zod'; export { definePage } from './ui/page.zod'; export { defineAction } from './ui/action.zod'; diff --git a/packages/spec/src/kernel/metadata-type-schemas.ts b/packages/spec/src/kernel/metadata-type-schemas.ts index 8d8c7fa603..ff8100bbd3 100644 --- a/packages/spec/src/kernel/metadata-type-schemas.ts +++ b/packages/spec/src/kernel/metadata-type-schemas.ts @@ -47,7 +47,6 @@ import { DatasetSchema } from '../ui/dataset.zod'; import { FlowSchema } from '../automation/flow.zod'; import { WebhookSchema } from '../automation/webhook.zod'; -import { ThemeSchema } from '../ui/theme.zod'; import { CubeSchema } from '../data/analytics.zod'; @@ -209,8 +208,11 @@ const BUILTIN_METADATA_TYPE_SCHEMAS: Partial> = * with `.strict()` schemas (`defineStack({ themes, analyticsCubes })`), named * by the URL-spelling contract as legal, addressable kinds — yet neither was * bound here, so `PUT /meta/theme/:name` stored ANY JSON with `success: true` - * while the stack door strictly refused the same body. They are bound now, the - * same SHAPE-check-only way. `rag_pipeline` is deliberately NOT bound: it has + * while the stack door strictly refused the same body. They were bound then, + * the same SHAPE-check-only way. (#10485 later retired the `themes` carrier + * and `ThemeSchema` whole under ADR-0049, and `theme` left the URL-spelling + * contract with it — see the entry-shaped comment below.) `rag_pipeline` is + * deliberately NOT bound: it has * no stack collection to take a schema from — that drift is recorded as #6242 * row 2 (`scripts/check-stack-collection-maps.mjs`), not solved by inventing a * schema here. @@ -261,16 +263,14 @@ const UNREGISTERED_KIND_SCHEMAS: Record = { // never refuses one. Same treatment #5271/#5312 gave their strict surfaces. sharing_rule: SharingRuleSchema, - // [#10194] `stack.zod.ts`: `themes: z.array(ThemeSchema)`. - // - // The console's own styling surface: a malformed theme stored through this - // door used to fail at RENDER, with nothing at the write point to say so. - // `ThemeSchema` declares the ADR-0010 envelope for the reason - // `sharing.zod.ts` states for its own spread — the schema is `.strict()`, - // and binding the door without the spread would aim the new 422 at the - // runtime's own `applyProtection` stamp instead of at malformed author - // input. - theme: ThemeSchema, + // [#10194 → #10485] `theme: ThemeSchema` was bound here when `themes` was a + // stack collection; #10485 retired that carrier and the schema whole + // (ADR-0049 — nothing downstream ever read a stored theme). The binding did + // NOT regress to the store-anything branch this docblock describes: with the + // `themes: 'theme'` fold gone from `PLURAL_TO_SINGULAR`, `theme` leaves the + // generated URL-spelling contract entirely, so `PUT /meta/theme/:name` now + // gets `unrecognisedMetaTypeRefusal`'s loud verdict (#8421) before any + // schema would be consulted. // [#10194] `stack.zod.ts`: `analyticsCubes: z.array(CubeSchema)`. // diff --git a/packages/spec/src/meta-spelling/meta-url-data.generated.ts b/packages/spec/src/meta-spelling/meta-url-data.generated.ts index c237b1a091..e87a0ec394 100644 --- a/packages/spec/src/meta-spelling/meta-url-data.generated.ts +++ b/packages/spec/src/meta-spelling/meta-url-data.generated.ts @@ -24,7 +24,6 @@ export const META_URL_TO_SINGULAR: Readonly> = Object.fre "reports": "report", "datasets": "dataset", "actions": "action", - "themes": "theme", "flows": "flow", "jobs": "job", "positions": "position", diff --git a/packages/spec/src/meta-spelling/metadata-url-spelling.ts b/packages/spec/src/meta-spelling/metadata-url-spelling.ts index f06ce9a697..c81d3893ec 100644 --- a/packages/spec/src/meta-spelling/metadata-url-spelling.ts +++ b/packages/spec/src/meta-spelling/metadata-url-spelling.ts @@ -55,8 +55,11 @@ * * 1. **Manifest spellings** — every key of `PLURAL_TO_SINGULAR`, verbatim. * These are spellings that already worked at the URL boundary, including - * the camelCase ones and the six that name PLUGIN-registered kinds with no - * static registry entry at all (`themes`, `webhooks`, `connectors`, …). + * the camelCase ones and the five that name PLUGIN-registered kinds with no + * static registry entry at all (`webhooks`, `connectors`, … — `themes` was + * one of them until #10485 retired the carrier, and left this limb WITH + * the `PLURAL_TO_SINGULAR` row, which is how a retired kind exits the + * spelling contract without this module changing). * Keeping this limb whole is what makes the derivation non-breaking: no * spelling that resolved before resolves differently now. * 2. **Registry-derived spellings** — {@link restPluralOfMetaType} applied to @@ -254,12 +257,15 @@ export function metaUrlSpellingRefusal( * {@link META_URL_TO_SINGULAR} rather than its keys. * * Strictly larger than `DECLARED_META_TYPES`, and that difference is the whole - * reason this set exists: limb 1 carries six kinds that NO registry derivation - * could produce — `theme`, `webhook`, `connector`, `sharing_rule`, - * `analytics_cube`, `rag_pipeline` — which are legal, addressable metadata - * kinds with no static registry entry. A refusal quantified over the registry - * alone would refuse all six, i.e. break `PUT /meta/theme/dark`, which is the - * exact operation the plugin path exists to serve. + * reason this set exists: limb 1 carries five kinds that NO registry derivation + * could produce — `webhook`, `connector`, `sharing_rule`, `analytics_cube`, + * `rag_pipeline` — which are legal, addressable metadata kinds with no static + * registry entry. A refusal quantified over the registry alone would refuse + * all five, i.e. break `PUT /meta/webhook/stripe`, which is the exact + * operation the plugin path exists to serve. (`theme` was the sixth until + * #10485 retired its carrier; dropping the `PLURAL_TO_SINGULAR` row is what + * moved `/meta/theme` from this set to `unrecognisedMetaTypeRefusal`'s + * verdict.) * * Module-internal (#8424), for the same reason `DECLARED_META_TYPES` is: it * LOOKS like a live registry of registered types and is not one. diff --git a/packages/spec/src/migrations/entries/retired-defs/18.ui__BorderRadius.ts b/packages/spec/src/migrations/entries/retired-defs/18.ui__BorderRadius.ts new file mode 100644 index 0000000000..6bdf8aded6 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-defs/18.ui__BorderRadius.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10485 — `ui/BorderRadius` (the border-radius scale sub-block) left with `ui/Theme`: +// its ONLY consumer was the retired `ThemeSchema` (the #3950 rule — an +// exported value schema with no consumer reads as a capability). See +// `18.ui__Theme.ts` for the retirement record and the ruling. +export const entry = 'ui/BorderRadius'; diff --git a/packages/spec/src/migrations/entries/retired-defs/18.ui__ColorPalette.ts b/packages/spec/src/migrations/entries/retired-defs/18.ui__ColorPalette.ts new file mode 100644 index 0000000000..4148347aec --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-defs/18.ui__ColorPalette.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10485 — `ui/ColorPalette` (the colour palette sub-block) left with `ui/Theme`: +// its ONLY consumer was the retired `ThemeSchema` (the #3950 rule — an +// exported value schema with no consumer reads as a capability). See +// `18.ui__Theme.ts` for the retirement record and the ruling. +export const entry = 'ui/ColorPalette'; diff --git a/packages/spec/src/migrations/entries/retired-defs/18.ui__Shadow.ts b/packages/spec/src/migrations/entries/retired-defs/18.ui__Shadow.ts new file mode 100644 index 0000000000..341a2a80b6 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-defs/18.ui__Shadow.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10485 — `ui/Shadow` (the shadow scale sub-block) left with `ui/Theme`: +// its ONLY consumer was the retired `ThemeSchema` (the #3950 rule — an +// exported value schema with no consumer reads as a capability). See +// `18.ui__Theme.ts` for the retirement record and the ruling. +export const entry = 'ui/Shadow'; diff --git a/packages/spec/src/migrations/entries/retired-defs/18.ui__Theme.ts b/packages/spec/src/migrations/entries/retired-defs/18.ui__Theme.ts new file mode 100644 index 0000000000..263c7d64f8 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-defs/18.ui__Theme.ts @@ -0,0 +1,20 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10485 — `ui/theme.zod.ts` `ThemeSchema`, retired whole with its +// `defineStack({ themes })` carrier key (ADR-0049 enforce-or-remove; +// maintainer ruling 2026-08-21, disposition B: 退役授权面 — `app.branding` +// stays the one colour surface; objectui's ThemeEngine/ThemeContext and their +// unit tests are retained). The pipeline was live from the authoring gate +// through artifact ingest and stopped there: zero non-test readers of the +// stored items across core/runtime/rest/services/plugins, `theme` never a +// registered metadata type, no first-party app mounting the spec-aware +// provider, and nothing selecting an active theme — an authored theme shipped +// green and changed nothing on screen. The carrier's strict-parse rejection +// carries the prescription (stack.zod.ts `guidance`); upgraders get the D3 +// semantic entry `stack-themes-carrier-retired`. +// +// Registered under 18, not 17: v17.0.0 was cut before this landed, so the +// removal ships on the 17.x line (launch-window convention: accept-set +// narrowings ride minor releases) and the prescription lives at the major +// boundary where `migrate meta` users look (the #8586 / #8715 precedent). +export const entry = 'ui/Theme'; diff --git a/packages/spec/src/migrations/entries/retired-defs/18.ui__ThemeMode.ts b/packages/spec/src/migrations/entries/retired-defs/18.ui__ThemeMode.ts new file mode 100644 index 0000000000..26d40dd4bc --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-defs/18.ui__ThemeMode.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10485 — `ui/ThemeMode` (the theme mode enum (light/dark/auto)) left with `ui/Theme`: +// its ONLY consumer was the retired `ThemeSchema` (the #3950 rule — an +// exported value schema with no consumer reads as a capability). See +// `18.ui__Theme.ts` for the retirement record and the ruling. +export const entry = 'ui/ThemeMode'; diff --git a/packages/spec/src/migrations/entries/retired-defs/18.ui__Typography.ts b/packages/spec/src/migrations/entries/retired-defs/18.ui__Typography.ts new file mode 100644 index 0000000000..fc4a169dcb --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-defs/18.ui__Typography.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10485 — `ui/Typography` (the typography sub-block) left with `ui/Theme`: +// its ONLY consumer was the retired `ThemeSchema` (the #3950 rule — an +// exported value schema with no consumer reads as a capability). See +// `18.ui__Theme.ts` for the retirement record and the ruling. +export const entry = 'ui/Typography'; diff --git a/packages/spec/src/migrations/entries/semantic/18.stack-themes-carrier-retired.ts b/packages/spec/src/migrations/entries/semantic/18.stack-themes-carrier-retired.ts new file mode 100644 index 0000000000..5d4a640b23 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.stack-themes-carrier-retired.ts @@ -0,0 +1,50 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +// Registered as D3 SEMANTIC and deliberately NOT as a D2 conversion, on the +// D2 scope guard (lossless only — the `owd-legacy-read-aliases` / `'full'` +// precedent): an authored theme has no lossless target. `app.branding` holds +// two colour strings scoped per APP, while a theme is a stack-scoped palette +// with modes and inheritance — a stack may declare N themes and M apps, so +// which palette entry becomes which app's `primaryColor` is a judgment the +// transform cannot make, and auto-DELETING the whole authored artifact would +// silently discard content the author may want to salvage. (Mechanically, a +// full-carrier `stripKeys` conversion also cannot coexist with the published +// `theme-inert-token-scales-removed` fixture: the fixture-disjointness +// contract replays the whole table over every fixture, and that entry's +// fixture necessarily authors `themes` — a secondary observation recorded for +// the next reader, not the reason.) +export const entry: SemanticMigration = { + id: 'stack-themes-carrier-retired', + surface: 'stack `themes` (the carrier collection, and `ThemeSchema` with its sub-blocks)', + replacement: + 'delete the `themes:` key (and any `defineTheme` calls). To colour the shipped console, ' + + "set `app.branding.primaryColor` / `accentColor` — the one live colour surface (read by " + + 'objectui, driving `--primary`, `--accent` and their derived CSS variables). A palette ' + + 'value your own stylesheet consumed has no spec slot any more: move it into your own CSS.', + reason: + 'ADR-0049 enforce-or-remove; maintainer ruling 2026-08-21 on #10485 (disposition B: ' + + '退役授权面 — objectui engine code and its unit tests are retained). The pipeline was ' + + 'live from the authoring gate (`ObjectStackDefinitionSchema.themes`, `defineTheme`) ' + + 'through artifact ingest (`ARTIFACT_FIELD_TO_TYPE.themes`) and stopped there, measured: ' + + 'zero non-test readers of `.themes` or stored `theme` items across ' + + 'core/runtime/rest/services/plugins; `theme` never in `MetadataTypeSchema`, ' + + '`DEFAULT_METADATA_TYPE_REGISTRY` or `BUILTIN_METADATA_TYPE_SCHEMAS`; the only mounted ' + + 'ThemeProvider is the app-shell chrome light/dark toggle, unrelated to `ThemeSchema`; ' + + 'and no key anywhere selected an active theme. So an author (human or AI) who wrote a ' + + 'theme shipped it through every green gate and saw nothing change — the ' + + 'declared-but-unenforced shape ADR-0049 exists to delete. What colours a console today ' + + 'is `app.branding`, and that path is live and untouched.', + acceptanceCriteria: + 'No stack source authors `themes:`; a stack that still does is refused at parse with the ' + + 'prescription (unrecognized_keys carrying the #10485 guidance — pinned in ' + + '`stack-top-level-strict.test.ts`). `PUT /meta/theme/:name` gets the #8421 ' + + 'unrecognised-type refusal instead of the pre-#10194 store-anything branch (pinned in ' + + '`protocol.unrecognised-meta-type.test.ts`). Legacy stored `theme` rows are untouched: ' + + '`applyConversionsToStoredItem` passes them through, reads still answer, and DELETE ' + + 'still works, so the residue is removable. ⚠️ On-screen behaviour is deliberately ' + + 'UNCHANGED and must be verified as such: nothing ever read an authored theme, so ' + + 'removing the surface removes no behaviour — `app.branding` colours the console before ' + + 'and after.', +}; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 9bc12a62cf..1a238c339d 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -5100,7 +5100,18 @@ const step18: MigrationStep = { 'conversion strips the key from old sources (pure lossless delete — it never had an ' + 'effect to lose); filter at query time with `where`, fold the condition into the ' + 'metric\'s own `sql` expression, or use an ADR-0021 dataset measure\'s structured ' + - '`filter`.', + '`filter`. ' + + 'Finally, it retires the stack `themes` carrier and `ThemeSchema` whole (#10485, ' + + 'ADR-0049 enforce-or-remove; maintainer ruling 2026-08-21, disposition B: 退役授权面): ' + + 'the pipeline was live from the authoring gate through artifact ingest and stopped ' + + 'there — zero non-test readers of stored `theme` items, `theme` never a registered ' + + 'metadata type, no first-party app mounting the spec-aware provider, nothing ' + + 'selecting an active theme — so an authored theme shipped through every green gate ' + + 'and changed nothing on screen. `app.branding` stays the one colour surface; ' + + 'objectui\'s ThemeEngine/ThemeContext and their unit tests are retained. Semantic ' + + 'rather than mechanical: an authored palette has no lossless target (N themes vs ' + + 'M apps is a judgment), so the entry prescribes the hand move instead of deleting ' + + 'authored content silently.', conversionIds: [ 'field-malformed-scale-precision-removed', 'record-chatter-position-vocabulary', @@ -5971,6 +5982,52 @@ const step18: MigrationStep = { + '`collapsible: true` explicitly — an unset key now defers to the renderer, which does ' + 'not collapse.', }, + // Registered as D3 SEMANTIC and deliberately NOT as a D2 conversion, on the + // D2 scope guard (lossless only — the `owd-legacy-read-aliases` / `'full'` + // precedent): an authored theme has no lossless target. `app.branding` holds + // two colour strings scoped per APP, while a theme is a stack-scoped palette + // with modes and inheritance — a stack may declare N themes and M apps, so + // which palette entry becomes which app's `primaryColor` is a judgment the + // transform cannot make, and auto-DELETING the whole authored artifact would + // silently discard content the author may want to salvage. (Mechanically, a + // full-carrier `stripKeys` conversion also cannot coexist with the published + // `theme-inert-token-scales-removed` fixture: the fixture-disjointness + // contract replays the whole table over every fixture, and that entry's + // fixture necessarily authors `themes` — a secondary observation recorded for + // the next reader, not the reason.) + { + id: 'stack-themes-carrier-retired', + surface: 'stack `themes` (the carrier collection, and `ThemeSchema` with its sub-blocks)', + replacement: + 'delete the `themes:` key (and any `defineTheme` calls). To colour the shipped console, ' + + "set `app.branding.primaryColor` / `accentColor` — the one live colour surface (read by " + + 'objectui, driving `--primary`, `--accent` and their derived CSS variables). A palette ' + + 'value your own stylesheet consumed has no spec slot any more: move it into your own CSS.', + reason: + 'ADR-0049 enforce-or-remove; maintainer ruling 2026-08-21 on #10485 (disposition B: ' + + '退役授权面 — objectui engine code and its unit tests are retained). The pipeline was ' + + 'live from the authoring gate (`ObjectStackDefinitionSchema.themes`, `defineTheme`) ' + + 'through artifact ingest (`ARTIFACT_FIELD_TO_TYPE.themes`) and stopped there, measured: ' + + 'zero non-test readers of `.themes` or stored `theme` items across ' + + 'core/runtime/rest/services/plugins; `theme` never in `MetadataTypeSchema`, ' + + '`DEFAULT_METADATA_TYPE_REGISTRY` or `BUILTIN_METADATA_TYPE_SCHEMAS`; the only mounted ' + + 'ThemeProvider is the app-shell chrome light/dark toggle, unrelated to `ThemeSchema`; ' + + 'and no key anywhere selected an active theme. So an author (human or AI) who wrote a ' + + 'theme shipped it through every green gate and saw nothing change — the ' + + 'declared-but-unenforced shape ADR-0049 exists to delete. What colours a console today ' + + 'is `app.branding`, and that path is live and untouched.', + acceptanceCriteria: + 'No stack source authors `themes:`; a stack that still does is refused at parse with the ' + + 'prescription (unrecognized_keys carrying the #10485 guidance — pinned in ' + + '`stack-top-level-strict.test.ts`). `PUT /meta/theme/:name` gets the #8421 ' + + 'unrecognised-type refusal instead of the pre-#10194 store-anything branch (pinned in ' + + '`protocol.unrecognised-meta-type.test.ts`). Legacy stored `theme` rows are untouched: ' + + '`applyConversionsToStoredItem` passes them through, reads still answer, and DELETE ' + + 'still works, so the residue is removable. ⚠️ On-screen behaviour is deliberately ' + + 'UNCHANGED and must be verified as such: nothing ever read an authored theme, so ' + + 'removing the surface removes no behaviour — `app.branding` colours the console before ' + + 'and after.', + }, { id: 'stack-top-level-unknown-keys-refused', surface: 'top-level stack definition keys (`ObjectStackDefinitionSchema`) — undeclared keys', @@ -6958,6 +7015,49 @@ export const RETIRED_DEFS_BY_MAJOR: Readonly> // narrowings ride minor releases) and the prescription lives at the major // boundary where `migrate meta` users look (the #8586 / PR #8702 precedent). 'identity/ApiKey', + // #10485 — `ui/BorderRadius` (the border-radius scale sub-block) left with `ui/Theme`: + // its ONLY consumer was the retired `ThemeSchema` (the #3950 rule — an + // exported value schema with no consumer reads as a capability). See + // `18.ui__Theme.ts` for the retirement record and the ruling. + 'ui/BorderRadius', + // #10485 — `ui/ColorPalette` (the colour palette sub-block) left with `ui/Theme`: + // its ONLY consumer was the retired `ThemeSchema` (the #3950 rule — an + // exported value schema with no consumer reads as a capability). See + // `18.ui__Theme.ts` for the retirement record and the ruling. + 'ui/ColorPalette', + // #10485 — `ui/Shadow` (the shadow scale sub-block) left with `ui/Theme`: + // its ONLY consumer was the retired `ThemeSchema` (the #3950 rule — an + // exported value schema with no consumer reads as a capability). See + // `18.ui__Theme.ts` for the retirement record and the ruling. + 'ui/Shadow', + // #10485 — `ui/theme.zod.ts` `ThemeSchema`, retired whole with its + // `defineStack({ themes })` carrier key (ADR-0049 enforce-or-remove; + // maintainer ruling 2026-08-21, disposition B: 退役授权面 — `app.branding` + // stays the one colour surface; objectui's ThemeEngine/ThemeContext and their + // unit tests are retained). The pipeline was live from the authoring gate + // through artifact ingest and stopped there: zero non-test readers of the + // stored items across core/runtime/rest/services/plugins, `theme` never a + // registered metadata type, no first-party app mounting the spec-aware + // provider, and nothing selecting an active theme — an authored theme shipped + // green and changed nothing on screen. The carrier's strict-parse rejection + // carries the prescription (stack.zod.ts `guidance`); upgraders get the D3 + // semantic entry `stack-themes-carrier-retired`. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // removal ships on the 17.x line (launch-window convention: accept-set + // narrowings ride minor releases) and the prescription lives at the major + // boundary where `migrate meta` users look (the #8586 / #8715 precedent). + 'ui/Theme', + // #10485 — `ui/ThemeMode` (the theme mode enum (light/dark/auto)) left with `ui/Theme`: + // its ONLY consumer was the retired `ThemeSchema` (the #3950 rule — an + // exported value schema with no consumer reads as a capability). See + // `18.ui__Theme.ts` for the retirement record and the ruling. + 'ui/ThemeMode', + // #10485 — `ui/Typography` (the typography sub-block) left with `ui/Theme`: + // its ONLY consumer was the retired `ThemeSchema` (the #3950 rule — an + // exported value schema with no consumer reads as a capability). See + // `18.ui__Theme.ts` for the retirement record and the ruling. + 'ui/Typography', // ], }; diff --git a/packages/spec/src/shared/metadata-collection.zod.ts b/packages/spec/src/shared/metadata-collection.zod.ts index d3e5b1274d..67defd69af 100644 --- a/packages/spec/src/shared/metadata-collection.zod.ts +++ b/packages/spec/src/shared/metadata-collection.zod.ts @@ -79,7 +79,7 @@ export const MAP_SUPPORTED_FIELDS = [ 'reports', 'datasets', 'actions', - 'themes', + // `themes` left with the retired carrier key (#10485, ADR-0049). 'flows', 'jobs', 'positions', @@ -116,7 +116,13 @@ export const PLURAL_TO_SINGULAR: Record = { reports: 'report', datasets: 'dataset', actions: 'action', - themes: 'theme', + // `themes: 'theme'` was removed at #10485 (ADR-0049; the carrier key is + // retired). Its absence here is load-bearing twice over: the generated + // `META_URL_TO_SINGULAR` (gen:meta-url-spelling) loses the fold, so + // `/meta/theme` gets `unrecognisedMetaTypeRefusal`'s loud verdict instead of + // the pre-#10194 store-anything branch; and `applyConversionsToStoredItem` + // passes legacy `theme` rows through untouched rather than manufacturing a + // collection for them. flows: 'flow', jobs: 'job', positions: 'position', diff --git a/packages/spec/src/stack-top-level-strict.test.ts b/packages/spec/src/stack-top-level-strict.test.ts index 64645fd3a2..e029a20919 100644 --- a/packages/spec/src/stack-top-level-strict.test.ts +++ b/packages/spec/src/stack-top-level-strict.test.ts @@ -100,6 +100,9 @@ describe('#8687 — unknown top-level stack keys are refused at parse', () => { ['workflows', 'state_machine'], // #3464: the collection was removed outright. ['portals', '#3464'], + // #10485 (ADR-0049): the themes carrier retired; app.branding is the + // one colour surface. + ['themes', 'app.branding'], // #4212: the uninvoked lifecycle family. ['onDisable', '#4212'], ]; @@ -122,6 +125,51 @@ describe('#8687 — unknown top-level stack keys are refused at parse', () => { }); }); +describe('#10485 — the `themes` carrier is retired (ADR-0049; ruled 退役授权面 2026-08-21)', () => { + // A theme body that parsed green on 17.0 — the exact authoring the issue + // measured shipping end-to-end while changing nothing on screen. + const previouslyAcceptedThemes = [ + { name: 'corporate', label: 'Corporate', mode: 'light', colors: { primary: '#7C3AED' } }, + ]; + + it('a stack declaring `themes` is refused LOUDLY, at the key (not the value)', () => { + const result = parseTopLevel({ ...legalStack(), themes: previouslyAcceptedThemes }); + expect(result.success).toBe(false); + if (result.success) return; + const issue = result.error.issues.find((i) => i.code === 'unrecognized_keys')!; + expect(issue, 'themes must raise unrecognized_keys').toBeDefined(); + expect(issue.path).toEqual([]); + expect((issue as unknown as { keys: string[] }).keys).toContain('themes'); + // The prescription itself — removal citation, and where colour lives now. + expect(issue.message).toContain('#10485'); + expect(issue.message).toContain('app.branding'); + expect(issue.message).toContain('primaryColor'); + // A retirement is a prescription, never a rename suggestion (finding 7). + expect(issue.message).not.toContain('Did you mean'); + }); + + it('positive control: the prescribed replacement parses green', () => { + const result = parseTopLevel({ + ...legalStack(), + apps: [{ + name: 'probe_app', + label: 'Probe', + branding: { primaryColor: '#7C3AED', accentColor: '#06B6D4' }, + }], + }); + expect(result.success, 'app.branding must stay accepted').toBe(true); + }); + + it('the schema surface is gone with the carrier: no theme export survives on ./ui', async () => { + const ui = await import('./ui/index'); + for (const name of ['ThemeSchema', 'ColorPaletteSchema', 'TypographySchema', 'BorderRadiusSchema', 'ShadowSchema', 'ThemeModeSchema', 'defineTheme']) { + expect(name in ui, `${name} must not survive on ./ui`).toBe(false); + } + // Anti-vacuity: the namespace probed is real and non-trivial. + expect('PageSchema' in ui).toBe(true); + }); +}); + describe('#8687 — the accept side does not move', () => { it('accepts every declared top-level key', () => { // Structural sweep: a declared key must never be refused as unknown. All diff --git a/packages/spec/src/stack.zod.ts b/packages/spec/src/stack.zod.ts index 8d6a0bd582..109c8a2ba0 100644 --- a/packages/spec/src/stack.zod.ts +++ b/packages/spec/src/stack.zod.ts @@ -29,7 +29,6 @@ import { DashboardSchema } from './ui/dashboard.zod'; import { ReportSchema } from './ui/report.zod'; import { DatasetSchema } from './ui/dataset.zod'; import { ActionSchema, InlineActionSchema } from './ui/action.zod'; -import { ThemeSchema } from './ui/theme.zod'; // Automation Protocol import { FlowSchema } from './automation/flow.zod'; @@ -224,6 +223,13 @@ export const ObjectStackDefinitionSchema = lazySchema(() => strictObject({ portals: 'the top-level `portals` collection was removed (#3464) — nothing ever consumed it. ' + 'Author external-user UI with `apps`/`views` plus positions and permission sets.', + themes: + '`themes` was removed in @objectstack/spec 17.1 (#10485, ADR-0049) — authored themes ' + + 'were parsed and stored, but no framework package ever read them back, no first-party ' + + 'app mounted the spec-aware theme provider, and nothing selected an active theme, so ' + + 'a declared theme changed nothing on screen. Delete the key. To colour the shipped ' + + 'console, set `app.branding.primaryColor` / `accentColor` — the one live colour ' + + 'surface (it drives `--primary`, `--accent` and their derived variables).', onDisable: 'no kernel, runtime or service ever called `onDisable` (#4212 retired the uninvoked ' + 'lifecycle family), so a value written here goes nowhere. Do teardown inside the ' @@ -336,7 +342,15 @@ export const ObjectStackDefinitionSchema = lazySchema(() => strictObject({ reports: z.array(ReportSchema).optional().describe('Analytics Reports'), datasets: z.array(DatasetSchema).optional().describe('Analytics semantic-layer datasets (ADR-0021)'), actions: z.array(ActionSchema).optional().describe('Global and Object Actions'), - themes: z.array(ThemeSchema).optional().describe('UI Themes'), + // `themes` was REMOVED in 17.1 (#10485, ADR-0049 enforce-or-remove — ruled + // 退役授权面, 2026-08-21). The pipeline was live from authoring gate through + // artifact ingest and stopped there: no framework package ever read the + // stored items, `theme` was never a registered metadata type, no first-party + // app mounted the spec-aware provider, and nothing selected a theme — an + // authored theme shipped green and changed nothing. `app.branding` is the + // one colour surface. The unknown-key rejection carries the prescription via + // the `guidance` entry on this schema's `strictObject` options; the D3 + // semantic entry `stack-themes-carrier-retired` carries it to upgraders. /** * ObjectFlow: Automation Layer @@ -1788,7 +1802,8 @@ const COMPOSE_KEY_DISPOSITIONS: Record, z export type Iso698 = Assert, z.infer< typeof M165.StyleMapSchema > >>; export type Iso699 = Assert, z.infer< typeof M165.ResponsiveStylesSchema > >>; -// ui/theme.zod.ts -export type Iso700 = Assert, z.infer< typeof M166.ColorPaletteSchema > >>; -export type Iso701 = Assert, z.infer< typeof M166.TypographySchema > >>; -export type Iso702 = Assert, z.infer< typeof M166.BorderRadiusSchema > >>; -export type Iso703 = Assert, z.infer< typeof M166.ShadowSchema > >>; -export type Iso704 = Assert, z.infer< typeof M166.ThemeModeSchema > >>; +// ui/theme.zod.ts (Iso700–Iso704) left with the module at #10485 — the Iso +// numbers are positional and stay vacant. // ui/view.zod.ts export type Iso705 = Assert, z.infer< typeof M167.FormButtonConfigSchema > >>; diff --git a/packages/spec/src/ui/index.ts b/packages/spec/src/ui/index.ts index 97ec9a2123..be6415cf21 100644 --- a/packages/spec/src/ui/index.ts +++ b/packages/spec/src/ui/index.ts @@ -41,7 +41,17 @@ export * from './page.zod'; export * from './widget.zod'; export * from './component.zod'; export * from './react-blocks'; -export * from './theme.zod'; +// `theme.zod.ts` was RETIRED WHOLE at #10485 (ADR-0049 enforce-or-remove; +// maintainer ruling 2026-08-21: 退役授权面 — `app.branding` is the one colour +// surface; objectui's ThemeEngine/ThemeContext and their unit tests are +// retained). `ThemeSchema`, its sub-blocks (`ColorPaletteSchema`, +// `TypographySchema`, `BorderRadiusSchema`, `ShadowSchema`, `ThemeModeSchema`) +// and `defineTheme` are gone with the `defineStack({ themes })` carrier key: +// the pipeline was live from the authoring gate through artifact ingest and +// stopped there — no framework package read the stored items, `theme` was +// never a registered metadata type, and nothing ever selected a theme. The +// carrier's rejection carries the prescription (stack.zod.ts `guidance`); +// upgraders get the D3 semantic entry `stack-themes-carrier-retired`. // `notification.zod` still exports the three presentation enums // (`NotificationType` / `NotificationSeverity` / `NotificationPosition`); // `NotificationActionSchema` / `NotificationAction` were REMOVED at #5015 per diff --git a/packages/spec/src/ui/interaction-config-retirement.test.ts b/packages/spec/src/ui/interaction-config-retirement.test.ts index c43aba49c3..47ac6defe2 100644 --- a/packages/spec/src/ui/interaction-config-retirement.test.ts +++ b/packages/spec/src/ui/interaction-config-retirement.test.ts @@ -111,8 +111,8 @@ describe('[#4988] ui/ interaction config family retirement', () => { * - `NotificationTypeSchema` — `ui/notification.zod.ts` kept its presentation * enums when PR #5300 retired `NotificationActionSchema` out of it. * - `SharingConfigSchema` — `ui/sharing.zod.ts`'s live door. - * - `ThemeSchema` — the file whose own `animation` block #5021 retired; the - * name collision must not have cost it its schema. + * - (`ThemeSchema` stood here until #10485 retired the theme surface whole, + * ADR-0049 — a survivor list entry follows its subject out.) * - `PageSchema` / `PageComponentSchema` — the authoring roots the five * vocabularies would have hung off had they ever had a carrier. */ @@ -122,7 +122,6 @@ describe('[#4988] ui/ interaction config family retirement', () => { 'I18nLabelSchema', 'NotificationTypeSchema', 'SharingConfigSchema', - 'ThemeSchema', 'PageSchema', 'PageComponentSchema', ] as const; diff --git a/packages/spec/src/ui/theme.test.ts b/packages/spec/src/ui/theme.test.ts deleted file mode 100644 index cd71c6634d..0000000000 --- a/packages/spec/src/ui/theme.test.ts +++ /dev/null @@ -1,810 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - ThemeSchema, - ThemeMode, - ThemeModeSchema, - ColorPaletteSchema, - TypographySchema, - BorderRadiusSchema, - ShadowSchema, - defineTheme, - type Theme, - type ColorPalette, -} from './theme.zod'; -import { ObjectStackSchema } from '../stack.zod'; - -describe('ThemeMode', () => { - it('should accept valid theme modes', () => { - expect(() => ThemeMode.parse('light')).not.toThrow(); - expect(() => ThemeMode.parse('dark')).not.toThrow(); - expect(() => ThemeMode.parse('auto')).not.toThrow(); - }); - - it('should reject invalid theme modes', () => { - expect(() => ThemeMode.parse('custom')).toThrow(); - expect(() => ThemeMode.parse('system')).toThrow(); - }); -}); - -describe('ColorPaletteSchema', () => { - it('should accept minimal color palette with primary color', () => { - const palette: ColorPalette = { - primary: '#007BFF', - }; - - expect(() => ColorPaletteSchema.parse(palette)).not.toThrow(); - }); - - it('should accept complete color palette', () => { - const palette: ColorPalette = { - primary: '#007BFF', - secondary: '#6C757D', - accent: '#FFC107', - success: '#28A745', - warning: '#FFC107', - error: '#DC3545', - info: '#17A2B8', - background: '#FFFFFF', - surface: '#F8F9FA', - text: '#212529', - textSecondary: '#6C757D', - border: '#DEE2E6', - disabled: '#E9ECEF', - primaryLight: '#4DA3FF', - primaryDark: '#0056B3', - }; - - expect(() => ColorPaletteSchema.parse(palette)).not.toThrow(); - }); - - it('should accept colors in different formats', () => { - const palette: ColorPalette = { - primary: '#007BFF', // hex - secondary: 'rgb(108, 117, 125)', // rgb - accent: 'hsl(45, 100%, 51%)', // hsl - }; - - expect(() => ColorPaletteSchema.parse(palette)).not.toThrow(); - }); -}); - -describe('TypographySchema', () => { - it('should accept minimal typography settings', () => { - const typography = { - fontFamily: { - base: 'Inter, system-ui, sans-serif', - }, - }; - - expect(() => TypographySchema.parse(typography)).not.toThrow(); - }); - - // REPLACED at #5021 (fixture triage, disposition 3). The fixture that used to - // sit here authored all four retired scales plus `fontFamily.heading`/`.mono` - // and asserted `not.toThrow()` — i.e. it pinned exactly the limbs the - // retirement deletes. Re-spelling it was not an option (there is no canonical - // spelling to move to) and keeping it would have made the whole surviving - // block untested, so it is replaced by a fixture the narrowed schema really - // reads. The rejection side is pinned in the #5021 block at the bottom. - it('accepts the whole SURVIVING typography surface — which is `fontFamily.base`', () => { - const typography = { fontFamily: { base: 'Inter, system-ui, sans-serif' } }; - - const parsed = TypographySchema.parse(typography); - expect(parsed.fontFamily?.base).toBe('Inter, system-ui, sans-serif'); - // Nothing else is left to WRITE: `base` emits `--font-sans`, the one - // typography variable objectui actually reads. (The four retired scales are - // still in the shape as tombstones, but they accept nothing, so they never - // appear on a parsed value.) - expect(Object.keys(parsed)).toEqual(['fontFamily']); - }); -}); - -describe('BorderRadiusSchema', () => { - it('should accept border radius scale', () => { - const borderRadius = { - none: '0', - sm: '0.125rem', - base: '0.25rem', - md: '0.375rem', - lg: '0.5rem', - full: '9999px', - }; - - expect(() => BorderRadiusSchema.parse(borderRadius)).not.toThrow(); - }); -}); - -describe('ShadowSchema', () => { - it('should accept shadow definitions', () => { - const shadows = { - none: 'none', - sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', - base: '0 1px 3px 0 rgba(0, 0, 0, 0.1)', - md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)', - lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)', - }; - - expect(() => ShadowSchema.parse(shadows)).not.toThrow(); - }); -}); - -describe('ThemeSchema', () => { - it('should accept minimal theme with required fields', () => { - const theme: Theme = { - name: 'default_theme', - label: 'Default Theme', - colors: { - primary: '#007BFF', - }, - }; - - expect(() => ThemeSchema.parse(theme)).not.toThrow(); - }); - - it('should enforce snake_case for theme name', () => { - const validNames = ['default_theme', 'dark_theme', 'custom_2023']; - validNames.forEach(name => { - const theme: Theme = { - name, - label: 'Test Theme', - colors: { primary: '#000000' }, - }; - expect(() => ThemeSchema.parse(theme)).not.toThrow(); - }); - - const invalidNames = ['DefaultTheme', 'dark-theme', '123theme']; - invalidNames.forEach(name => { - const theme = { - name, - label: 'Test Theme', - colors: { primary: '#000000' }, - }; - expect(() => ThemeSchema.parse(theme)).toThrow(); - }); - }); - - it('should accept complete theme configuration', () => { - const theme: Theme = { - name: 'enterprise_theme', - label: 'Enterprise Theme', - description: 'Professional theme for enterprise applications', - mode: 'light', - colors: { - primary: '#0066CC', - secondary: '#6C757D', - accent: '#FFC107', - success: '#28A745', - warning: '#FFC107', - error: '#DC3545', - info: '#17A2B8', - background: '#FFFFFF', - surface: '#F8F9FA', - text: '#212529', - textSecondary: '#6C757D', - border: '#DEE2E6', - }, - // #5021 fixture triage, disposition 1 (re-spell): this fixture merely - // USED `fontFamily.heading`/`.mono` and a `fontSize` scale, so it drops - // them and keeps the live `base`. It is still a "complete" theme — the - // completeness that matters is the set of blocks with live consumers. - typography: { - fontFamily: { - base: 'Inter, sans-serif', - }, - }, - borderRadius: { - base: '0.25rem', - lg: '0.5rem', - }, - shadows: { - base: '0 1px 3px 0 rgba(0, 0, 0, 0.1)', - lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)', - }, - }; - - expect(() => ThemeSchema.parse(theme)).not.toThrow(); - }); - - it('should accept theme with custom CSS variables', () => { - const theme: Theme = { - name: 'custom_vars_theme', - label: 'Custom Variables Theme', - colors: { - primary: '#007BFF', - }, - customVars: { - '--header-height': '64px', - '--sidebar-width': '256px', - '--transition-speed': '0.3s', - }, - }; - - expect(() => ThemeSchema.parse(theme)).not.toThrow(); - }); - - it('should accept theme that extends another theme', () => { - const theme: Theme = { - name: 'dark_extended', - label: 'Dark Extended Theme', - extends: 'default_theme', - colors: { - primary: '#007BFF', - background: '#1A1A1A', - text: '#FFFFFF', - }, - }; - - expect(() => ThemeSchema.parse(theme)).not.toThrow(); - }); - - it('should apply default mode', () => { - const theme = { - name: 'test_theme', - label: 'Test Theme', - colors: { - primary: '#007BFF', - }, - }; - - const result = ThemeSchema.parse(theme); - expect(result.mode).toBe('light'); - }); - - it('should accept dark mode theme', () => { - const theme: Theme = { - name: 'dark_theme', - label: 'Dark Theme', - mode: 'dark', - colors: { - primary: '#4DA3FF', - background: '#1A1A1A', - surface: '#2D2D2D', - text: '#FFFFFF', - textSecondary: '#B0B0B0', - border: '#404040', - }, - }; - - expect(() => ThemeSchema.parse(theme)).not.toThrow(); - }); - - it('should accept auto mode theme', () => { - const theme: Theme = { - name: 'auto_theme', - label: 'Auto Theme', - mode: 'auto', - colors: { - primary: '#007BFF', - }, - }; - - expect(() => ThemeSchema.parse(theme)).not.toThrow(); - }); - - // The `zIndex` and `animation` acceptance fixtures that sat here were - // REPLACED at #5021 (fixture triage, disposition 3) — they pinned the two - // keys the retirement deletes, and an `expect(…).not.toThrow()` on a deleted - // key has no honest re-spelling. Their replacements are the layering and - // motion fixtures below, which express the SAME intent through the door that - // survived, plus the rejection pins in the #5021 block at the bottom. - - it('a layering scale is still expressible — through `customVars`, the live door', () => { - const theme: Theme = { - name: 'layered_theme', - label: 'Layered Theme', - colors: { primary: '#007BFF' }, - // The variable names are spelled out because that is what the engine used - // to derive from the `zIndex` key names. Byte for byte the same custom - // properties reach the document — which is the whole basis on which the - // retirement claims to remove no capability. - customVars: { - 'z-base': '0', - 'z-dropdown': '1000', - 'z-sticky': '1020', - 'z-fixed': '1030', - 'z-modal-backdrop': '1040', - 'z-modal': '1050', - 'z-popover': '1060', - 'z-tooltip': '1070', - }, - }; - - const parsed = ThemeSchema.parse(theme); - expect(parsed.customVars?.['z-modal']).toBe('1050'); - }); - - it('a motion scale is still expressible — same door, same variables', () => { - const theme: Theme = { - name: 'animated_theme', - label: 'Animated Theme', - colors: { primary: '#007BFF' }, - customVars: { - 'duration-fast': '150ms', - 'duration-base': '300ms', - 'duration-slow': '500ms', - 'timing-ease': 'cubic-bezier(0.4, 0, 0.2, 1)', - 'timing-ease_in': 'cubic-bezier(0.4, 0, 1, 1)', - }, - }; - - const parsed = ThemeSchema.parse(theme); - expect(parsed.customVars?.['duration-fast']).toBe('150ms'); - }); - -}); - -describe('Real-World Theme Examples', () => { - it('should accept enterprise light theme', () => { - const theme: Theme = { - name: 'enterprise_light', - label: 'Enterprise Light', - description: 'Professional light theme for enterprise applications', - mode: 'light', - colors: { - primary: '#0066CC', - secondary: '#4A5568', - accent: '#ED8936', - success: '#48BB78', - warning: '#ECC94B', - error: '#F56565', - info: '#4299E1', - background: '#FFFFFF', - surface: '#F7FAFC', - text: '#1A202C', - textSecondary: '#718096', - border: '#E2E8F0', - }, - // #5021 fixture triage, disposition 1 (re-spell): `heading` dropped, the - // live `base` kept. - typography: { - fontFamily: { - base: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif', - }, - }, - }; - - expect(() => ThemeSchema.parse(theme)).not.toThrow(); - }); - - it('should accept dark theme with extended configuration', () => { - const theme: Theme = { - name: 'professional_dark', - label: 'Professional Dark', - mode: 'dark', - colors: { - primary: '#60A5FA', - secondary: '#9CA3AF', - accent: '#FBBF24', - success: '#34D399', - warning: '#FBBF24', - error: '#F87171', - info: '#60A5FA', - background: '#0F172A', - surface: '#1E293B', - text: '#F1F5F9', - textSecondary: '#94A3B8', - border: '#334155', - }, - borderRadius: { - base: '0.375rem', - lg: '0.5rem', - xl: '0.75rem', - }, - shadows: { - base: '0 1px 3px 0 rgba(0, 0, 0, 0.3)', - md: '0 4px 6px -1px rgba(0, 0, 0, 0.3)', - lg: '0 10px 15px -3px rgba(0, 0, 0, 0.3)', - }, - }; - - expect(() => ThemeSchema.parse(theme)).not.toThrow(); - }); -}); - -// ============================================================================ -// Issue #6: Easing naming unified to snake_case in theme animation tokens -// -// RETIRED at #5021. `AnimationSchema` no longer exists, so the snake_case-vs- -// camelCase question this block settled is moot at the schema — there is no -// declared easing vocabulary left to be inconsistent about. The old fixture -// (fixture triage, disposition 3) pinned `animation.timing` directly. -// -// Kept as a pin rather than deleted, because the useful half of #6's finding -// SURVIVES the retirement and would otherwise go untested: an author who wants -// snake_case easing variables can still emit them, and now spells the variable -// name in full instead of relying on the engine's key-to-variable derivation — -// which is the one thing that actually changed for them. -// ============================================================================ -describe('easing tokens after the #5021 retirement', () => { - it('the snake_case easing vocabulary is still emittable through `customVars`', () => { - const theme = ThemeSchema.parse({ - name: 'snake_case_timing', - label: 'Snake Case Timing', - colors: { primary: '#000' }, - customVars: { - 'timing-linear': 'linear', - 'timing-ease': 'ease', - 'timing-ease_in': 'ease-in', - 'timing-ease_out': 'ease-out', - 'timing-ease_in_out': 'ease-in-out', - }, - }); - expect(theme.customVars?.['timing-ease_in']).toBe('ease-in'); - expect(theme.customVars?.['timing-ease_in_out']).toBe('ease-in-out'); - }); - - it('`animation` itself is gone — the key no longer exists on the parsed theme', () => { - const theme = ThemeSchema.parse({ - name: 'no_animation', - label: 'No Animation', - colors: { primary: '#000' }, - }); - expect(theme).not.toHaveProperty('animation'); - expect(theme).not.toHaveProperty('zIndex'); - }); -}); - -// ============================================================================ -// Issue #9: ThemeModeSchema — canonical *Schema name with deprecated alias -// ============================================================================ -describe('ThemeModeSchema (canonical name)', () => { - it('should accept valid theme modes', () => { - expect(() => ThemeModeSchema.parse('light')).not.toThrow(); - expect(() => ThemeModeSchema.parse('dark')).not.toThrow(); - expect(() => ThemeModeSchema.parse('auto')).not.toThrow(); - }); - - it('should be the same as deprecated ThemeMode alias', () => { - expect(ThemeModeSchema).toBe(ThemeMode); - }); -}); - -// ============================================================================ -// #4001 批 15 — unknown keys are REJECTED, and the rejection is fixable. -// -// These assertions are the third of the three places this batch's verdict is -// recorded (the other two: the header comment in `theme.zod.ts`, and the ui/ -// row in `docs/audits/2026-07-unknown-key-strictness-ledger.md`). They pin -// three separate things, because each can regress on its own: -// -// 1. THE DOOR. Strictness is a property of a PARSE; a strict schema nobody -// parses gates nothing (#4583). So the first block asserts the parse -// exists — `defineTheme()` and `defineStack({ themes })` — rather than -// only asserting the schema is strict. -// 2. EVERY ONE of the 14 object sites is closed. Strictness does NOT recurse -// (the 批 13 finding: a strict shell around strip sub-blocks is the -// silhouette of a closed surface, not a closed surface), so each nested -// block is probed at its own path. -// 3. The CURATION — the aliases and tombstones that make the rejection -// fixable. Each entry here was measured against a named sibling contract, -// not guessed; the comments in `theme.zod.ts` say which. -// ============================================================================ -describe('#4001 批 15 — ThemeSchema unknown-key strictness', () => { - const base = { name: 'brand_theme', label: 'Brand', colors: { primary: '#000000' } }; - const reject = (theme: unknown): string => { - const r = ThemeSchema.safeParse(theme); - expect(r.success, 'expected this theme to be REJECTED').toBe(false); - return JSON.stringify(r.error?.issues ?? []); - }; - - it('the control parses — these tests fail closed, not by rejecting everything', () => { - expect(ThemeSchema.safeParse(base).success).toBe(true); - }); - - // ---- 1. the door ---------------------------------------------------- - it('defineTheme() is a real parse door — it throws on an undeclared key', () => { - expect(() => defineTheme({ ...base, spacing: {} } as never)).toThrow(/Unrecognized key/); - }); - - it('defineStack({ themes }) is the second door — ObjectStackSchema carries ThemeSchema', () => { - const shape = (ObjectStackSchema as unknown as { _zod: { def: { shape: Record } } })._zod.def.shape; - expect(Object.keys(shape), 'the carrier key this file is reachable through').toContain('themes'); - }); - - // ---- 2. all fourteen sites, each at its own path --------------------- - it('rejects an undeclared key at the TOP level', () => { - expect(reject({ ...base, extraKey: 1 })).toContain('extraKey'); - }); - - it('rejects an undeclared key in `colors` — strictness does not stop at the shell', () => { - expect(reject({ ...base, colors: { primary: '#000', notAColor: '#fff' } })).toContain('notAColor'); - }); - - // #5021 shrank this list from fourteen sites to six. The `animation`, - // `zIndex`, `typography.fontSize`/`.fontWeight`/`.lineHeight`/ - // `.letterSpacing` and `animation.duration`/`.timing` rows were not - // re-spelled — those schemas no longer exist, so an unknown-key probe against - // them has nothing to probe. What replaced them is the retirement pin at the - // bottom of this file: writing the BLOCK is now the rejection, which is - // strictly stronger than rejecting one bad key inside it. - it.each([ - ['typography', { notATypographyKey: 1 }], - ['borderRadius', { notARadius: '1px' }], - ['shadows', { notAShadow: 'x' }], - ])('rejects an undeclared key in `%s`', (block, value) => { - expect(reject({ ...base, [block]: value })).toContain(Object.keys(value)[0]); - }); - - it('rejects an undeclared key in the nested `typography.fontFamily` block', () => { - expect(reject({ ...base, typography: { fontFamily: { notAFamily: 'x' } } })).toContain('notAFamily'); - }); - - // ---- 3. curation ---------------------------------------------------- - it('renames the shadcn colour vocabulary onto the palette keys it maps to', () => { - // MEASURED against objectui's COLOR_TO_CSS_MAP: `surface` is emitted as - // `--card`, `text` as `--foreground`, `disabled` as `--muted`, `error` as - // `--destructive`. An author reading the rendered CSS writes the shadcn - // name back, which no edit distance can reach. - const msg = reject({ ...base, colors: { primary: '#000', card: '#fff', foreground: '#111', destructive: '#f00' } }); - expect(msg).toContain('`card` → `surface`'); - expect(msg).toContain('`foreground` → `text`'); - expect(msg).toContain('`destructive` → `error`'); - }); - - // The `md` → `base` (font-size), `base` → `normal` (font-weight) and - // `easeIn` → `ease_in` (animation timing) curation tests lived here until - // #5021. All three graded aliases INSIDE a retired scale, so they went with - // their schemas rather than being re-spelled — there is no surviving surface - // on which `md` or `easeIn` means anything. This is the honest reading of the - // fixture-triage rule: a fixture whose subject was deleted is replaced by one - // the surviving rule reads, not kept alive on a technicality. - - it('renames onto the camelCase targets the distance fallback is weak on (#4990)', () => { - // `findClosestMatches` lowercases the input but not the candidates, so a - // capital costs an edit. These land through the explicit table instead. - // - // The `backdrop` → `modalBackdrop` case that used to anchor this test was - // on `zIndex` and retired with it (#5021); `radius` and `cssVars` carry the - // same property on surfaces that are still live. - expect(reject({ ...base, radius: {} })).toContain('`radius` → `borderRadius`'); - expect(reject({ ...base, cssVars: {} })).toContain('`cssVars` → `customVars`'); - expect(reject({ ...base, customProperties: {} })).toContain('`customProperties` → `customVars`'); - }); - - it('renames `inset` onto `inner` — CSS\'s word for what this scale calls inner', () => { - expect(reject({ ...base, shadows: { inset: '0 0 1px' } })).toContain('`inset` → `inner`'); - }); - - it('carries a TOMBSTONE, not a rename, for each of the eight props #3494 removed', () => { - for (const key of ['spacing', 'breakpoints', 'logo', 'density', 'wcagContrast', 'rtl', 'touchTarget', 'keyboardNavigation']) { - const msg = reject({ ...base, [key]: 'x' }); - expect(msg, `${key} must carry its own #3494 prescription`).toContain('#3494'); - expect(msg, `${key} must be named in its own prescription`).toContain('`' + key + '` was removed'); - } - }); - - it('gives each retired key its OWN sentence — a shared string prints N times (批 10)', () => { - const msg = reject({ ...base, rtl: true, density: 'compact' }); - expect(msg).toContain('text direction follows the document'); - expect(msg).toContain('compact/comfortable spacing'); - // Two keys, two DISTINCT bullets. - expect(msg.split('• ').length - 1).toBe(2); - }); - - it('never prescribes a vocabulary with no carrier key (the ledger\'s finding 7)', () => { - // `touchTarget` / `keyboardNavigation` look like they should point at - // `ui/touch.zod.ts` / `ui/keyboard.zod.ts`. 批 13 measured both as having - // NO carrier, so prescribing them would walk an author out of a loud - // rejection into a silent one. #4988 then RETIRED both modules, which makes - // this assertion stronger rather than moot: the strings they must not - // contain now name files that do not exist, so a future edit reaching for - // "a nearby-looking slot" would be prescribing a deleted module. - const msg = reject({ ...base, touchTarget: 1, keyboardNavigation: true }); - expect(msg).not.toContain('touch.zod'); - expect(msg).not.toContain('keyboard.zod'); - }); - - it('every suggestion it makes is a key the schema actually accepts', () => { - // The `triggerPhrases` lesson in `shared/strict-object.ts`: never point an - // author at a key that will reject them a second time. Walk the whole - // alias table and prove each target parses. - // - // `animation` and `zIndex` left this list at #5021 — WITH the five aliases - // that pointed at them (`animations`/`motion`/`transitions`/`layers`/ - // `stacking`). That pairing is the point of the test, not bookkeeping: had - // the aliases stayed, this assertion would be the thing that caught it. - const targets = ['colors', 'typography', 'borderRadius', 'shadows', 'customVars', 'extends', 'label', 'name', 'mode']; - const shape = (ThemeSchema as unknown as { _zod: { def: { shape: Record } } })._zod.def.shape; - for (const t of targets) expect(Object.keys(shape), `alias target "${t}" must be declared`).toContain(t); - }); -}); - -// ============================================================================ -// #5021 — the nine emitted-but-unread token groups, RETIRED (ADR-0049 D2) -// -// The measurement (objectui `main`, re-confirmed 2026-08-04): `--font-size-*`, -// `--font-weight-*`, `--line-height-*`, `--letter-spacing-*`, `--duration-*`, -// `--timing-*`, `--z-*`, `--font-heading` and `--font-mono` have ZERO consumers -// across objectui's `packages/**`, while `--font-sans`, `--radius*`, -// `--shadow*` and the colour variables are read — the positive controls that -// make the zero mean something. -// -// Route: STRICT REMOVAL + guidance map, not a `retiredKey()` tombstone. Both -// channels are still covered and it is worth being explicit about which does -// what, because the two routes' evidence looks different: -// * `tsc` — the key is gone from the inferred input type, so authoring one -// fails to compile. (A `retiredKey()` would type it `never`; deleting it -// from a `.strict()` shape is the same outcome by a different mechanism.) -// * the parse — `strictObject`'s `guidance` map carries the prescription, so -// the rejection is the upgrade instruction rather than a bare -// "unrecognized key". That is what these tests pin. -// ============================================================================ -describe('#5021 — retired theme token scales', () => { - const base = { name: 'demo_theme', label: 'Demo', colors: { primary: '#000' } }; - const reject = (input: unknown): string => { - const r = ThemeSchema.safeParse(input); - expect(r.success, 'fixture must be REJECTED — a passing parse means the key is still live').toBe(false); - return r.error!.issues.map((i) => i.message).join('\n'); - }; - - // ---- 1. the control: this suite fails closed ------------------------ - it('the surviving theme parses — these tests reject specific keys, not everything', () => { - expect(ThemeSchema.safeParse({ - ...base, - typography: { fontFamily: { base: 'Inter' } }, - borderRadius: { base: '0.25rem' }, - shadows: { base: '0 1px 2px rgba(0,0,0,.1)' }, - customVars: { 'z-modal': '1050' }, - }).success).toBe(true); - }); - - // ---- 2. every retired key rejects, at its own path ------------------- - it('rejects `animation` and `zIndex` at the theme top level', () => { - expect(reject({ ...base, animation: { duration: { fast: '150ms' } } })).toContain('`theme.animation` was removed'); - expect(reject({ ...base, zIndex: { modal: 1050 } })).toContain('`theme.zIndex` was removed'); - }); - - it.each(['fontSize', 'fontWeight', 'lineHeight', 'letterSpacing'])( - 'rejects the retired `typography.%s` scale', - (key) => { - const msg = reject({ ...base, typography: { [key]: {} } }); - expect(msg).toContain('`theme.typography.' + key + '` was removed'); - }, - ); - - it.each(['heading', 'mono'])('rejects the retired `typography.fontFamily.%s`', (key) => { - const msg = reject({ ...base, typography: { fontFamily: { base: 'Inter', [key]: 'Georgia' } } }); - expect(msg).toContain('`theme.typography.fontFamily.' + key + '` was removed'); - }); - - // ---- 3. the prescription is the payload ----------------------------- - it('every prescription names `customVars`, the door measured to have real consumers', () => { - for (const [input, key] of [ - [{ ...base, animation: {} }, 'animation'], - [{ ...base, zIndex: {} }, 'zIndex'], - [{ ...base, typography: { fontSize: {} } }, 'typography.fontSize'], - [{ ...base, typography: { fontWeight: {} } }, 'typography.fontWeight'], - [{ ...base, typography: { lineHeight: {} } }, 'typography.lineHeight'], - [{ ...base, typography: { letterSpacing: {} } }, 'typography.letterSpacing'], - [{ ...base, typography: { fontFamily: { heading: 'x' } } }, 'typography.fontFamily.heading'], - [{ ...base, typography: { fontFamily: { mono: 'x' } } }, 'typography.fontFamily.mono'], - ] as const) { - const msg = reject(input); - expect(msg, `${key} must prescribe customVars`).toContain('customVars'); - expect(msg, `${key} must name the migration command`).toContain('os migrate meta --from 16'); - expect(msg, `${key} must cite the issue`).toContain('#5021'); - } - }); - - it('gives each retired key its OWN sentence — a shared string prints N times (批 10)', () => { - const r = ThemeSchema.safeParse({ ...base, animation: {}, zIndex: {} }); - expect(r.success).toBe(false); - const msg = r.error!.issues.map((i) => i.message).join('\n'); - expect(msg).toContain('every transition ran at the renderer default'); - expect(msg).toContain('still stacked by document order'); - // Two keys, two DISTINCT issues. Note the shape difference from the #4001 - // block above: a `guidance` prescription arrives as ONE unrecognized-key - // issue carrying N bullets, whereas a `retiredKey()` raises its own issue - // per key — so this counts issues, not bullets. - expect(r.error!.issues).toHaveLength(2); - }); - - // ---- 4. finding 7: no suggestion may point at a retired key ---------- - it('the five aliases that pointed at `animation`/`zIndex` are GONE, not re-pointed', () => { - // Had `layers: 'zIndex'` survived, an author writing `layers` would be told - // "did you mean `zIndex`?" and then rejected for writing `zIndex` — walked - // out of one rejection into a second. The ledger's finding 7, which this - // file's own header has now signposted three times. - for (const alias of ['animations', 'motion', 'transitions', 'layers', 'stacking']) { - const msg = reject({ ...base, [alias]: {} }); - expect(msg, `${alias} must not be renamed onto a retired key`).not.toContain('→ `animation`'); - expect(msg, `${alias} must not be renamed onto a retired key`).not.toContain('→ `zIndex`'); - } - }); - - it('the typography aliases that pointed at retired scales are GONE too', () => { - for (const alias of ['sizes', 'size', 'weights', 'weight', 'tracking', 'leading']) { - const msg = reject({ ...base, typography: { [alias]: {} } }); - for (const dead of ['fontSize', 'fontWeight', 'lineHeight', 'letterSpacing']) { - expect(msg, `${alias} must not be renamed onto retired \`${dead}\``).not.toContain('→ `' + dead + '`'); - } - } - for (const alias of ['headings', 'display', 'monospace', 'code']) { - const msg = reject({ ...base, typography: { fontFamily: { base: 'Inter', [alias]: 'x' } } }); - expect(msg, `${alias} must not be renamed onto retired \`heading\``).not.toContain('→ `heading`'); - expect(msg, `${alias} must not be renamed onto retired \`mono\``).not.toContain('→ `mono`'); - } - }); - - it('the retired keys are DECLARED-but-unwritable, not deleted — the tombstone route', () => { - // The distinction that decides how `authorable-surface.json` moves: a - // tombstoned key STAYS in the walked shape (gaining a `[RETIRED]` marker - // in the baseline) rather than vanishing from it. Assert the mechanism - // directly, so a future "tidy-up" that deletes these keys outright fails - // here rather than in `gen:schema`'s #4650 deletion check. - const shapeOf = (s: unknown) => - (s as { _zod: { def: { shape: Record } } })._zod.def.shape; - - const themeShape = shapeOf(ThemeSchema); - const typoShape = shapeOf(TypographySchema); - expect(Object.keys(themeShape)).toContain('animation'); - expect(Object.keys(themeShape)).toContain('zIndex'); - expect(Object.keys(typoShape)).toEqual( - expect.arrayContaining(['fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing']), - ); - - // …and every one of them accepts NOTHING, which is what makes it a - // tombstone rather than a live key. - for (const [shape, keys] of [ - [themeShape, ['animation', 'zIndex']], - [typoShape, ['fontSize', 'fontWeight', 'lineHeight', 'letterSpacing']], - ] as const) { - for (const k of keys) { - const inner = (shape[k] as { _zod: { def: { innerType?: { _zod: { def: { type: string } } } } } }) - ._zod.def.innerType; - expect(inner?._zod.def.type, `${k} must be a never-typed tombstone`).toBe('never'); - } - } - }); - - // ---- 5. the capability survives ------------------------------------- - it('`customVars` reproduces every retired variable by name — capability is not lost', () => { - // This is the claim the whole retirement rests on, so it is pinned rather - // than asserted in prose: the engine emits `customVars` as `--: - // ` verbatim, so each retired variable has an exact spelling here. - const parsed = ThemeSchema.parse({ - ...base, - customVars: { - 'font-size-lg': '1.125rem', - 'font-weight-bold': '700', - 'line-height-relaxed': '1.75', - 'letter-spacing-wide': '0.025em', - 'duration-fast': '150ms', - 'timing-ease_in': 'cubic-bezier(0.4, 0, 1, 1)', - 'z-modal': '1050', - 'font-heading': 'Georgia, serif', - 'font-mono': 'ui-monospace, monospace', - }, - }); - // One entry per retired GROUP — all nine the issue measured. - expect(Object.keys(parsed.customVars ?? {})).toHaveLength(9); - }); - - // ---- 6. the live blocks are untouched ------------------------------- - it('the blocks with live consumers still parse — the retirement is scoped, not a sweep', () => { - const parsed = ThemeSchema.parse({ - ...base, - colors: { primary: '#7C3AED', surface: '#F8F9FA', text: '#1F2937' }, - typography: { fontFamily: { base: 'Inter, sans-serif' } }, - borderRadius: { none: '0', base: '0.25rem', full: '9999px' }, - shadows: { base: '0 1px 3px rgba(0,0,0,.1)', inner: 'inset 0 2px 4px rgba(0,0,0,.06)' }, - }); - expect(parsed.colors.surface).toBe('#F8F9FA'); - expect(parsed.typography?.fontFamily?.base).toBe('Inter, sans-serif'); - expect(parsed.borderRadius?.full).toBe('9999px'); - expect(parsed.shadows?.inner).toBe('inset 0 2px 4px rgba(0,0,0,.06)'); - }); - - // ---- 7. the authoring DOORS carry the rejection ---------------------- - it('both authoring doors reject a retired key — `defineTheme` and `defineStack`', () => { - expect(() => defineTheme({ ...base, zIndex: { modal: 1 } } as never)).toThrow(/`theme\.zIndex` was removed/s); - - const stack = ObjectStackSchema.safeParse({ - name: 'demo', label: 'Demo', - themes: [{ ...base, typography: { fontSize: { base: '1rem' } } }], - }); - expect(stack.success).toBe(false); - expect(stack.error!.issues.map((i) => i.message).join('\n')) - .toContain('`theme.typography.fontSize` was removed'); - }); -}); diff --git a/packages/spec/src/ui/theme.zod.ts b/packages/spec/src/ui/theme.zod.ts deleted file mode 100644 index 366e97f620..0000000000 --- a/packages/spec/src/ui/theme.zod.ts +++ /dev/null @@ -1,503 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { z } from 'zod'; -import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod'; - -// --------------------------------------------------------------------------- -// CLOSED AGAINST UNKNOWN KEYS (#4001 批 15, ADR-0078) — and the door was -// MEASURED before anything was tightened, not assumed. Read this before adding -// a key, and before "finishing" any sibling file by analogy. -// -// THE DOOR (three measurements, 2026-08-03, each with controls in the run): -// -// 1. CARRIER KEY — `stack.zod.ts` declares `themes: z.array(ThemeSchema)`, -// and `defineTheme()` (exported from the package root) parses a theme -// directly. Both are doors an author writes by hand. -// 2. GRAPH — a BFS over this build's in-memory Zod graph from all 24 -// metadata-type roots (`listMetadataTypeSchemaTypes`) plus -// `ObjectStackSchema` — the closure `build-schemas.ts` uses for the #4650 -// deletion check — reaches EVERY schema in this file (`ThemeSchema` and -// its sub-blocks `root-graph`, `ColorPaletteSchema` `derived-clone`). -// Controls in the same run: `PageSchema` / `DashboardSchema` / -// `ReportSchema` / `WebhookSchema` / `StateMachineSchema` all resolve, and -// 批 13's measured no-door shapes (`TouchTargetConfigSchema`, -// `GestureConfigSchema`) came back unreachable. So "reachable" here is a -// fact about the graph, not an instrument that says yes to everything. -// (Those two negative controls were RETIRED at #4988 — the whole no-door -// interaction family went — so a re-run supplies its own, e.g. an inline -// `z.object({ a: z.string() })`. The reading above is unaffected.) -// 3. PARSE — `defineStack()` parses `ObjectStackSchema` on every app boot and -// on every `objectstack build`, so a theme key is judged on the path an -// author actually runs. -// -// `theme` is deliberately NOT in `BUILTIN_METADATA_TYPE_SCHEMAS` — it is not -// a metadata KIND (#2657's B/C decision stays open). Since #10194 it IS bound -// in `UNREGISTERED_KIND_SCHEMAS` (the #6245 map), so the runtime metadata -// write door (`PUT /meta/theme/:name`) applies the SAME shape check -// `defineStack` / `defineTheme` do, instead of storing any JSON with -// `success: true`. That is a SHAPE check only — no registry entry, no -// capability or authorization change — and this comment does not claim more -// than that. -// -// WHY EVERY SUB-BLOCK IS `strict` AND NOT `passthrough` — the #4909 question, -// asked per block rather than per file, because the theme engine reads the two -// halves of this file DIFFERENTLY (`@object-ui/core`'s `ThemeEngine.ts`): -// -// * `colors` / `borderRadius` / `shadows` / `typography.fontFamily` are read -// through FIXED maps (`COLOR_TO_CSS_MAP`, the radius and shadow maps) or by -// named property. An undeclared key there is read by nothing, ever. -// * The token SCALES that used to sit beside them — `typography.fontSize` / -// `.fontWeight` / `.lineHeight` / `.letterSpacing`, `animation.duration` / -// `.timing` and `zIndex` — were read with `Object.entries(...)`, emitting -// `--font-size-`, `--duration-`, `--z-` … for whatever they -// were handed. That WAS the #4909 open shape at the runtime. It is moot -// now: all of them were RETIRED at #5021 (see the block below). -// * And the escape hatch it might otherwise have removed already exists and -// is declared: `customVars` emits an arbitrary CSS custom property by name. -// An author who wants `--font-size-huge` writes it there. Openness in the -// token scales would only add a second, undocumented way to do the same -// thing — one whose typos are indistinguishable from intent. -// -// ✅ ANSWERED AT #5021 (ADR-0049 enforce-or-remove) — this file's own filed -// question, now closed. The block above used to end "SEPARATE, FILED, NOT -// ANSWERED HERE", because 批 15 correctly refused to decide a LIVENESS question -// inside a STRICTNESS batch: strictness makes a dropped key loud, it cannot -// make a slot live. The measurement it filed on (re-confirmed against objectui -// `main` on 2026-08-04, with `--font-sans` / `--radius` / `--shadow` / -// `--primary` as positive controls in the same run) was that `--font-size-*`, -// `--font-weight-*`, `--line-height-*`, `--letter-spacing-*`, `--z-*`, -// `--duration-*`, `--timing-*`, `--font-heading` and `--font-mono` have ZERO -// first-party consumers, while only the colour variables, `--radius*`, -// `--shadow*` and `--font-sans` are read. -// -// The maintainer's ruling (2026-08-04) is RETIRE, not "add consumers" and not -// "promise them as a public token surface": theme-driven typography is not a -// near-term product capability, so wiring shadcn/Tailwind to read these would -// build a feature nobody asked for, and stamping a stability guarantee onto a -// surface that changes nothing in the platform's own UI is a promise attached -// to an inert slot (the #4583 shape — a precisely validated dead slot is the -// more convincing lie). -// -// ⚠️ The counter-argument that kept these alive through #3494 was answered, not -// ignored: a CSS custom property differs from an ordinary spec key because a -// TENANT'S OWN STYLESHEET can read it once it lands on the document, so -// "zero in-repo consumers" is weaker evidence here than it is elsewhere. That -// is exactly why the prescription is `customVars` rather than a bare deletion. -// `customVars` emits `--: ` verbatim, so a tenant who really was -// reading `--z-modal` or `--font-size-lg` from their own CSS reproduces every -// one of these variables BYTE FOR BYTE. Capability lost: none. What is lost is -// a semantic vocabulary that the platform itself never honoured — which is the -// thing ADR-0049 exists to delete. -// -// `colors`, `borderRadius`, `shadows` and `typography.fontFamily.base` have -// live consumers and are UNTOUCHED. objectui's `ThemeEngine` still emits the -// retired groups until its own stop-emitting follow-up lands (filed on the -// objectui queue); that is NOT a correctness dependency in either direction — -// an emitted variable nothing reads is inert, and after this change no author -// can put a value into one, so the emitter has nothing left to emit. -// --------------------------------------------------------------------------- - -/** - * Color Palette Schema - * Defines brand colors and their variants. - */ -import { lazySchema } from '../shared/lazy-schema'; -import { retiredKey } from '../shared/retired-key'; -import { strictObject } from '../shared/strict-object'; -import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; - -// Competing vocabulary, MEASURED not guessed: objectui's `COLOR_TO_CSS_MAP` -// renames every one of these on the way out (`surface` → `--card`, `text` → -// `--foreground`, `textSecondary` → `--muted-foreground`, `disabled` → -// `--muted`, `error` → `--destructive`). An author who read the rendered CSS — -// or who knows shadcn — writes the shadcn name back into the palette, which is -// a different WORD for the same intent rather than a typo, so edit distance -// cannot reach it. `aliasProbe` lowercases and strips `-`/`_`, so each entry -// also covers the `muted-foreground` spelling. -const COLOR_ALIASES: Readonly> = { - card: 'surface', - foreground: 'text', - mutedForeground: 'textSecondary', - muted: 'disabled', - destructive: 'error', - danger: 'error', - textPrimary: 'text', - secondaryText: 'textSecondary', -}; - -export const ColorPaletteSchema = lazySchema(() => strictObject( - { - surface: 'this theme color palette', - history: - 'Until #4001 an undeclared colour key was dropped at parse and every element kept its default colour — the brand change simply did not happen, and the theme still reported valid.', - aliases: COLOR_ALIASES, - }, - { - primary: z.string().describe('Primary brand color (hex, rgb, or hsl)'), - secondary: z.string().optional().describe('Secondary brand color'), - accent: z.string().optional().describe('Accent color for highlights'), - success: z.string().optional().describe('Success state color (default: green)'), - warning: z.string().optional().describe('Warning state color (default: yellow)'), - error: z.string().optional().describe('Error state color (default: red)'), - info: z.string().optional().describe('Info state color (default: blue)'), - - // Neutral colors - background: z.string().optional().describe('Background color'), - surface: z.string().optional().describe('Surface/card background color'), - text: z.string().optional().describe('Primary text color'), - textSecondary: z.string().optional().describe('Secondary text color'), - border: z.string().optional().describe('Border color'), - disabled: z.string().optional().describe('Disabled state color'), - - // Color variants (shades) - primaryLight: z.string().optional().describe('Lighter shade of primary'), - primaryDark: z.string().optional().describe('Darker shade of primary'), - secondaryLight: z.string().optional().describe('Lighter shade of secondary'), - secondaryDark: z.string().optional().describe('Darker shade of secondary'), - }, -)); - -/** - * Typography Settings Schema - * Base font family. The size / weight / line-height / letter-spacing scales - * were retired at #5021 and are `retiredKey()` tombstones below. - * - * ⚠️ WHY `retiredKey()` AND NOT A `guidance` ENTRY, on a shape that is already - * `.strict()`. The two routes are not interchangeable here and the build is - * what settles it. `guidance` is consulted for `unrecognized_keys`, which - * requires the key to be ABSENT from the shape — and a key absent from the - * shape is absent from `authorable-surface.json`, which is a ratcheted - * baseline. Deleting a live line from it fails `gen:schema`'s #4650 deletion - * check (`the entry at was LIVE (never tombstoned)`), and rightly: the - * ratchet cannot see that THIS parent happens to be strict, and the class of - * mistake it guards — a key deleted from a non-strict shape, silently stripped - * forever after — is indistinguishable at the file level. - * - * A tombstone satisfies both: the key stays in the walked shape (so the - * baseline gains a `[RETIRED]` marker instead of losing a line), and it is - * strictly LOUDER than the strict shell, because it carries its own - * prescription instead of a generic unknown-key message. `strictObject` already - * anticipates the combination — its `acceptsNothing()` helper exists precisely - * to keep a tombstoned key out of the "did you mean" candidates. - * - * Every prescription points at `customVars`, and that is a byte-for-byte - * replacement rather than a consolation prize: the engine emits `customVars` as - * `--: ` verbatim, so `customVars: { 'font-size-lg': '1.125rem' }` - * puts the SAME `--font-size-lg` on the document this block used to. What the - * author loses is a semantic vocabulary the platform never honoured; what they - * keep is every variable they were actually shipping. - */ -export const TypographySchema = lazySchema(() => strictObject( - { - surface: 'this theme typography block', - history: - 'Until #4001 an undeclared typography key was dropped at parse and the type scale silently stayed at the defaults.', - // ⚠️ The `sizes`/`size`, `weights`/`weight`, `tracking`/`spacing` and - // `leading` aliases were DELETED at #5021, not re-pointed. They named - // Tailwind's utility words for scales this block no longer declares, so - // keeping them would answer an author with "did you mean `fontSize`?" and - // then reject `fontSize` — walking them out of one rejection into a second - // one, which is the ledger's finding 7 (this campaign signposting the way - // into the failure mode it exists to kill). A key that no longer exists - // gets no suggestion at all; the four canonical spellings still get their - // full prescription through `guidance` below. - aliases: { - fonts: 'fontFamily', font: 'fontFamily', family: 'fontFamily', fontFamilies: 'fontFamily', - }, - }, - { - fontFamily: strictObject( - { - surface: 'this theme font-family block', - history: - 'Until #4001 an undeclared font-family key was dropped at parse and the element kept the system font stack.', - // MEASURED: the engine emits `base` as `--font-sans` (the only one of - // the three that any objectui stylesheet ever read — which is why it is - // the only one of the three that survived #5021), so the rendered - // variable name and the authorable key name disagree; `sans` is what an - // author reading the output writes back, four edits away from `base`. - // - // `headings`/`display` → `heading` and `monospace`/`code` → `mono` were - // DELETED at #5021 with their targets, for the finding-7 reason spelled - // out on the parent block. - aliases: { sans: 'base', body: 'base', default: 'base' }, - }, - { - base: z.string().optional().describe('Base font family (default: system fonts)'), - - heading: retiredKey( - '`theme.typography.fontFamily.heading` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — it emitted `--font-heading`, which no objectui component or stylesheet reads, so headings always rendered in the base font stack. `base` is the ONE font-family key with a live consumer (it emits `--font-sans`) and is unchanged. Delete the key; if your own CSS reads the variable, declare it under `customVars` (`{ "font-heading": "Georgia, serif" }` emits exactly the same `--font-heading`). ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', - ), - mono: retiredKey( - '`theme.typography.fontFamily.mono` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — it emitted `--font-mono`, which no objectui component or stylesheet reads, so code always rendered in the browser default monospace. `base` is the ONE font-family key with a live consumer (it emits `--font-sans`) and is unchanged. Delete the key; if your own CSS reads the variable, declare it under `customVars` (`{ "font-mono": "ui-monospace, monospace" }` emits exactly the same `--font-mono`). ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', - ), - }, - ).optional(), - - fontSize: retiredKey( - '`theme.typography.fontSize` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-size-xs` … `--font-size-4xl` faithfully and NO first-party component or stylesheet has ever read one, so a declared type scale was real CSS that styled nothing. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-size-lg": "1.125rem" }` emits exactly the same `--font-size-lg`). ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', - ), - fontWeight: retiredKey( - '`theme.typography.fontWeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-weight-*` and nothing read it, so text rendered at the inherited weight whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-weight-semibold": "600" }` emits exactly the same `--font-weight-semibold`). ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', - ), - lineHeight: retiredKey( - '`theme.typography.lineHeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--line-height-*` and nothing read it, so every block kept its inherited leading. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "line-height-relaxed": "1.75" }` emits exactly the same `--line-height-relaxed`). ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', - ), - letterSpacing: retiredKey( - '`theme.typography.letterSpacing` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--letter-spacing-*` and nothing read it, so tracking never moved. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "letter-spacing-wide": "0.025em" }` emits exactly the same `--letter-spacing-wide`). ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', - ), - }, -)); - -/** - * Border Radius Schema - * Rounded corners configuration. - */ -export const BorderRadiusSchema = lazySchema(() => strictObject( - { - surface: 'this theme border-radius scale', - history: - 'Until #4001 an undeclared radius stop was dropped at parse and the corners rendered at the default radius.', - // MEASURED: `base` is emitted as the BARE `--radius`, which is the one - // radius variable objectui's stylesheets actually read — so `radius` is - // what an author copying from the rendered CSS writes. - aliases: { radius: 'base', default: 'base', normal: 'base', pill: 'full', round: 'full', rounded: 'full' }, - }, - { - none: z.string().optional().describe('No border radius (0)'), - sm: z.string().optional().describe('Small border radius (e.g., 0.125rem)'), - base: z.string().optional().describe('Base border radius (e.g., 0.25rem)'), - md: z.string().optional().describe('Medium border radius (e.g., 0.375rem)'), - lg: z.string().optional().describe('Large border radius (e.g., 0.5rem)'), - xl: z.string().optional().describe('Extra large border radius (e.g., 0.75rem)'), - '2xl': z.string().optional().describe('2X large border radius (e.g., 1rem)'), - full: z.string().optional().describe('Full border radius (50%)'), - }, -)); - -/** - * Shadow Schema - * Box shadow effects. - */ -export const ShadowSchema = lazySchema(() => strictObject( - { - surface: 'this theme shadow scale', - history: - 'Until #4001 an undeclared shadow stop was dropped at parse and the surface rendered flat.', - // `inset` is CSS's own word for what this scale calls `inner` — and the - // key's own `.describe()` says so ("Inner shadow (inset)"). - aliases: { inset: 'inner', default: 'base', normal: 'base' }, - }, - { - none: z.string().optional().describe('No shadow'), - sm: z.string().optional().describe('Small shadow'), - base: z.string().optional().describe('Base shadow'), - md: z.string().optional().describe('Medium shadow'), - lg: z.string().optional().describe('Large shadow'), - xl: z.string().optional().describe('Extra large shadow'), - '2xl': z.string().optional().describe('2X large shadow'), - inner: z.string().optional().describe('Inner shadow (inset)'), - }, -)); - -/** - * Theme Mode Schema - */ -export const ThemeModeSchema = lazySchema(() => z.enum(['light', 'dark', 'auto'])); - -/** @deprecated Use ThemeModeSchema instead */ -export const ThemeMode = ThemeModeSchema; - -// Tombstones for the eight props #3494 removed, plus the two blocks #5021 -// retired. Each carries its OWN sentence: `guidance` prints one bullet per -// rejected key verbatim, so a shared string prints the same paragraph N times -// (批 10's `join`/`joinGateway` lesson). -// -// Two of them are deliberately worded NOT to hand the author a replacement -// slot. `touchTarget` and `keyboardNavigation` read like they should point at -// `ui/touch.zod.ts` / `ui/keyboard.zod.ts` — but 批 13 measured both of those -// vocabularies as having no carrier key at all, so prescribing them would walk -// an author out of a loud rejection and into a silent one. That is the ledger's -// finding 7, and this campaign has now signposted its own failure mode twice; -// it does not get to do it a third time. **#4988 settled it the other way**: -// both modules were RETIRED outright (ADR-0049), so the slot these two -// prescriptions declined to name no longer exists at all — the refusal was -// right, and it must stay a refusal rather than becoming a dangling pointer. -// -// The #5021 pair is the OPPOSITE case and it is worth keeping the distinction -// visible: `animation` and `zIndex` DO get a replacement slot, because -// `customVars` is measured live (the engine emits every entry verbatim) rather -// than merely plausible. That is the whole difference between a prescription -// and a signpost into a second rejection. -const THEME_RETIRED_KEY_GUIDANCE: Readonly> = { - spacing: - '`spacing` was removed in #3494 — the theme engine (objectui `generateThemeVars`) never emitted a spacing variable, so authoring it was a silent no-op. Emit your own scale through `customVars` (e.g. `{ "space-4": "1rem" }`).', - breakpoints: - '`breakpoints` was removed in #3494 — breakpoints are not theme-scoped. Author responsive behaviour per component (`page.components[].responsive`), where the protocol owns the breakpoint names.', - logo: - '`logo` was removed in #3494 — brand imagery is app-scoped, not theme-scoped: set `branding.logo` on the app (`ui/app.zod.ts`).', - density: - '`density` was removed in #3494 — no renderer read it. Express compact/comfortable spacing as your own tokens under `customVars`.', - wcagContrast: - '`wcagContrast` was removed in #3494 — it declared a check nothing ran. Contrast is measured against the palette you author (`contrastRatio` / `meetsContrastLevel` in the theme engine), never enabled by a flag.', - rtl: - '`rtl` was removed in #3494 — text direction follows the document and locale, not the theme.', - touchTarget: - '`touchTarget` was removed in #3494 — the theme engine never emitted a touch-target variable, so it changed nothing. If you need a token for it, declare one under `customVars`.', - keyboardNavigation: - '`keyboardNavigation` was removed in #3494 — keyboard behaviour is not a CSS variable and was never emitted; no theme key can switch it on or off.', -}; - -/** - * The two theme-level keys #5021 retired. - * - * These are `retiredKey()` tombstones rather than `guidance` entries — unlike - * the eight above, which predate the `authorable-surface.json` ratchet and are - * long gone from it. See the note on `TypographySchema` for why a strict shell - * does not make the tombstone redundant. - */ -const THEME_ANIMATION_RETIRED = - '`theme.animation` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — unlike the #3494 props above, the engine DID emit `--duration-*` and `--timing-*`, faithfully and for years; what never existed was a reader. No first-party component or stylesheet has ever consumed one, so every transition ran at the renderer default whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "duration-fast": "150ms", "timing-ease_in": "cubic-bezier(0.4, 0, 1, 1)" }` emits exactly the same properties). ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; - -const THEME_ZINDEX_RETIRED = - '`theme.zIndex` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--z-base` … `--z-tooltip` and nothing read one, so an overlay you "lifted" still stacked by document order. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "z-modal": "1050" }` emits exactly the same `--z-modal`). ' - + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.'; - -/** - * Theme Configuration Schema - * Complete theme definition for brand customization. - * - * #3494: the aspirational props `spacing`, `breakpoints`, `logo`, `density`, - * `wcagContrast`, `rtl`, `touchTarget` and `keyboardNavigation` were removed — - * the theme engine (objectui generateThemeVars) never consumed them, so - * authoring them was a silent no-op (liveness audit #1878/#1893). Since #4001 - * writing one is a loud rejection carrying its replacement, instead of a silent - * strip that made the removal indistinguishable from the bug it fixed. - * - * #5021 removed `animation` and `zIndex` (and, inside `typography`, the - * `fontSize` / `fontWeight` / `lineHeight` / `letterSpacing` scales and - * `fontFamily.heading` / `.mono`). #3494's criterion could not reach these: - * it was "the engine never emits it", and the engine emitted every one of - * these. The criterion that reaches them is ADR-0049's — emitted, but read by - * nobody. `colors`, `borderRadius`, `shadows` and `fontFamily.base` have live - * consumers and stay. - */ -export const ThemeSchema = lazySchema(() => strictObject( - { - surface: 'this theme', - history: - 'Until #4001 an undeclared theme key was dropped at parse and the theme loaded looking complete — which is exactly how the eight props #3494 removed went on being authored after they stopped existing.', - aliases: { - palette: 'colors', colours: 'colors', colorPalette: 'colors', color: 'colors', - fonts: 'typography', font: 'typography', type: 'typography', - radius: 'borderRadius', borderRadii: 'borderRadius', radii: 'borderRadius', - shadow: 'shadows', boxShadow: 'shadows', elevation: 'shadows', - // `animations`/`motion`/`transitions` → `animation` and - // `layers`/`stacking` → `zIndex` were DELETED at #5021 rather than - // re-pointed at `customVars`: an alias renames one key to another, and - // this is not a rename — a scale object would have to become a flat - // string map with the CSS variable names spelled out and the numbers - // stringified. Suggesting the retired key would hand the author a second - // rejection (finding 7); suggesting `customVars` would imply the value - // transfers unchanged, which it does not. The prescription in `guidance` - // states the shape change; the D2 conversion DELETES the key and emits a - // notice rather than auto-populating `customVars`, because a rewrite - // would silently reconstitute ~25 variables nothing reads as - // live-looking config — the retirement would be invisible in exactly the - // way ADR-0049 objects to. - cssVars: 'customVars', variables: 'customVars', vars: 'customVars', customProperties: 'customVars', tokens: 'customVars', - extend: 'extends', parent: 'extends', inherits: 'extends', basedOn: 'extends', - title: 'label', displayName: 'label', - id: 'name', key: 'name', - darkMode: 'mode', colorScheme: 'mode', scheme: 'mode', - }, - guidance: THEME_RETIRED_KEY_GUIDANCE, - }, - { - name: SnakeCaseIdentifierSchema.describe('Unique theme identifier (snake_case)'), - label: z.string().describe('Human-readable theme name'), - description: z.string().optional().describe('Theme description'), - - /** Theme mode */ - mode: ThemeModeSchema.default('light').describe('Theme mode (light, dark, or auto)'), - - /** Color system */ - colors: ColorPaletteSchema.describe('Color palette configuration'), - - /** Typography */ - typography: TypographySchema.optional().describe('Typography settings'), - - /** Border radius */ - borderRadius: BorderRadiusSchema.optional().describe('Border radius scale'), - - /** Shadows */ - shadows: ShadowSchema.optional().describe('Box shadow effects'), - - /** @deprecated REMOVED at #5021 — see {@link THEME_ANIMATION_RETIRED}. */ - animation: retiredKey(THEME_ANIMATION_RETIRED), - - /** @deprecated REMOVED at #5021 — see {@link THEME_ZINDEX_RETIRED}. */ - zIndex: retiredKey(THEME_ZINDEX_RETIRED), - - // `AnimationSchema` and `ZIndexSchema` were DELETED outright rather than - // left standing beside these tombstones: an exported value schema with no - // consumer reads as a capability to whoever finds it (#3950), and each had - // exactly one consumer — the key now tombstoned above. Their own - // `authorable-surface` lines leave with the def, which is the #4650 - // deletion check's third proof (whole def no longer emitted) and is - // adjudicated by the json-schema.manifest.json ratchet (#2978) instead. - - /** - * Custom CSS variables. - * - * The declared door for any custom property, and — since #5021 — the ONLY - * one. Each entry is emitted verbatim as `--: `, so this is - * where a `--z-modal` or a `--font-size-lg` goes now. - */ - customVars: z.record(z.string(), z.string()).optional().describe('Custom CSS variables (key-value pairs)'), - - /** Extends another theme */ - extends: z.string().optional().describe('Base theme to extend from'), - - // ADR-0010 — runtime protection envelope (internal — set by loader). - // - // [#10194] Declared for the reason `sharing.zod.ts` states for its own - // spread: BOTH metadata load paths call `applyProtection` on EVERY type, - // so a package-loaded theme already carries these keys by the time - // anything re-parses it. This shape is `.strict()`, so until now that - // stamped envelope was not merely dropped — it was REJECTED, which stayed - // invisible only because `theme` resolved no schema at the overlay door - // and nothing parsed it there. #10194 binds that door - // (`UNREGISTERED_KIND_SCHEMAS`), so declaring the envelope is what keeps - // the new 422 aimed at malformed AUTHOR input instead of at the runtime's - // own stamp. `metadata-type-schemas.test.ts` enforces exactly this. - ...MetadataProtectionFields, - }, -)); - -export type Theme = z.input; -/** Post-parse shape of {@link Theme} — defaults applied, transforms run (ADR-0122). */ -export type ThemeParsed = z.infer; - -/** - * Type-safe factory for a UI theme. Validates at authoring time via - * `.parse()` and accepts input-shape config (optional defaults, CEL - * shorthand) — preferred over a bare `: Theme` literal. - */ -export function defineTheme(config: z.input): ThemeParsed { - return ThemeSchema.parse(config); -} -export type ColorPalette = z.input; -export type Typography = z.input; -export type BorderRadius = z.input; -export type Shadow = z.input; -// `Animation` and `ZIndex` were exported here until #5021, alongside the two -// schemas they were inferred from. -export type ThemeMode = z.input; diff --git a/packages/spec/src/ui/widget-i18n-retirement.test.ts b/packages/spec/src/ui/widget-i18n-retirement.test.ts index 6248ddca98..efdb12cb13 100644 --- a/packages/spec/src/ui/widget-i18n-retirement.test.ts +++ b/packages/spec/src/ui/widget-i18n-retirement.test.ts @@ -108,7 +108,7 @@ describe('[#5055] ui/ widget + i18n family retirement', () => { 'ResponsiveConfigSchema', 'NotificationTypeSchema', 'SharingConfigSchema', - 'ThemeSchema', + // (`ThemeSchema` stood here until #10485 retired the theme surface whole.) 'PageSchema', 'PageComponentSchema', ] as const; diff --git a/scripts/check-stack-collection-maps.mjs b/scripts/check-stack-collection-maps.mjs index f955690fbe..5293af163f 100644 --- a/scripts/check-stack-collection-maps.mjs +++ b/scripts/check-stack-collection-maps.mjs @@ -455,9 +455,10 @@ const SITES = [ waivers: [ { direction: 'extra', - keys: ['workflows', 'approvals', 'roles', 'profiles', 'policies', 'ragPipelines'], + keys: ['workflows', 'approvals', 'roles', 'profiles', 'policies', 'ragPipelines', 'themes'], reason: - 'DRIFT — six kinds retired across ADR-0019 / ADR-0020 / ADR-0088 / ADR-0090 that this loop still ' + 'DRIFT — kinds retired across ADR-0019 / ADR-0020 / ADR-0088 / ADR-0090 / ADR-0049 (#10485 for ' + + '`themes`) that this loop still ' + 'iterates. Inert: `(manifest as any)[key]` is `undefined` for every one, because the schema ' + 'rejects the keys long before the loop runs. Removing them is an `engine-core` source change ' + '(#6242 row 3) and rides that lane, not this gate.', @@ -550,7 +551,7 @@ const SITES = [ { direction: 'missing', keys: [ - 'objectExtensions', 'datasourceMapping', 'datasources', 'themes', 'jobs', 'apis', 'webhooks', + 'objectExtensions', 'datasourceMapping', 'datasources', 'jobs', 'apis', 'webhooks', 'hooks', 'mappings', 'analyticsCubes', 'connectors', 'capabilities', 'datasets', ], reason: @@ -579,7 +580,7 @@ const SITES = [ waivers: [ { direction: 'missing', - keys: ['datasourceMapping', 'translations', 'themes', 'data', 'sharingRules', 'webhooks'], + keys: ['datasourceMapping', 'translations', 'data', 'sharingRules', 'webhooks'], reason: 'NOT DEMONSTRATED, and visible for the first time. This manifest tracked 3 of the non-kind ' + 'collections and — unlike its ratcheted sibling `KIND_COVERAGE` — was answerable to nothing ' diff --git a/skills/objectstack-platform/SKILL.md b/skills/objectstack-platform/SKILL.md index 07b88b555a..4a72b59604 100644 --- a/skills/objectstack-platform/SKILL.md +++ b/skills/objectstack-platform/SKILL.md @@ -176,7 +176,7 @@ export default defineStack({ `defineStack()` accepts an `ObjectStackDefinitionInput`. Each top-level key holds a collection of one metadata kind — `manifest`, `objects`, `objectExtensions`, `views`, `apps`, `pages`, `dashboards`, -`reports`, `datasets`, `actions`, `themes`, `flows`, `jobs`, +`reports`, `datasets`, `actions`, `flows`, `jobs`, `emailTemplates`, `docs`, `books`, `positions`, `permissions`, `capabilities`, `sharingRules`, `apis`, `webhooks`, `api`, `agents`, `tools`, `skills`, `hooks`, `functions`, `mappings`, `analyticsCubes`, diff --git a/skills/objectstack-ui/references/_index.md b/skills/objectstack-ui/references/_index.md index c5034313ba..01f52e6a46 100644 --- a/skills/objectstack-ui/references/_index.md +++ b/skills/objectstack-ui/references/_index.md @@ -17,7 +17,6 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/ui/dataset.zod.ts` — Analytics Dataset — the one semantic layer (ADR-0021). - `node_modules/@objectstack/spec/src/ui/page.zod.ts` — Page Region Schema - `node_modules/@objectstack/spec/src/ui/report.zod.ts` — Report Type Enum -- `node_modules/@objectstack/spec/src/ui/theme.zod.ts` — Color Palette Schema - `node_modules/@objectstack/spec/src/ui/view.zod.ts` — HTTP Method Enum & HTTP Request Schema - `node_modules/@objectstack/spec/src/ui/widget.zod.ts` — Field Widget Props Schema From 2db934d618ba73c32e7a9b539b54e7857f31179b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 08:01:37 +0000 Subject: [PATCH 2/5] test: retriage theme specimens across consumer suites; light gates green Specimen swaps (theme -> webhook) in metadata-protocol, objectql, rest and runtime meta-door tests; new #10485 refusal pins; stack-collection-maps waivers updated; PROTOCOL_MAP row removed; Iso pin count 839 -> 834. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw --- .../src/protocol.code-only-types.test.ts | 20 ++++--- packages/objectql/src/protocol-meta.test.ts | 59 ++++++++++++++----- .../meta-unknown-type-read-refusal.test.ts | 23 +++++++- .../src/meta-compound-arity-mint-door.test.ts | 13 ++-- .../src/meta-field-overlay-lock.test.ts | 24 ++++---- .../runtime/src/meta-write-org-scope.test.ts | 5 +- packages/spec/PROTOCOL_MAP.md | 1 - .../src/kernel/metadata-type-schemas.test.ts | 4 +- .../src/shared/metadata-collection.test.ts | 2 +- .../src/shared/metadata-url-spelling.test.ts | 49 ++++++++++----- .../src/type-alias-convention.pin.test.ts | 11 +++- scripts/check-stack-collection-maps.mjs | 6 +- 12 files changed, 147 insertions(+), 70 deletions(-) diff --git a/packages/metadata-protocol/src/protocol.code-only-types.test.ts b/packages/metadata-protocol/src/protocol.code-only-types.test.ts index e09d783ba8..3f4e1add61 100644 --- a/packages/metadata-protocol/src/protocol.code-only-types.test.ts +++ b/packages/metadata-protocol/src/protocol.code-only-types.test.ts @@ -396,14 +396,15 @@ describe('code-only metadata types are refused on every kernel (#5086)', () => { // `getMetaTypes()` synthesises those with allowRuntimeCreate:true; // the write gate must keep agreeing with what it advertises. const { protocol, rows } = makeProtocol(undefined); - // [#10194] `theme` resolves a schema through UNREGISTERED_KIND_SCHEMAS - // now, so the probe body must be spec-valid — the door under test + // [#6245] `webhook` resolves a schema through UNREGISTERED_KIND_SCHEMAS, + // so the probe body must be spec-valid — the door under test // (authorization) is unchanged, but a malformed body would 422 - // before proving anything about it. + // before proving anything about it. (`theme` was the specimen until + // #10485 retired that kind out of the spelling contract.) const result = await protocol.saveMetaItem({ - type: 'theme', - name: 'rc3_probe_theme', - item: { name: 'rc3_probe_theme', label: 'Probe', colors: { primary: '#3b82f6' } }, + type: 'webhook', + name: 'rc3_probe_webhook', + item: { name: 'rc3_probe_webhook', label: 'Probe', object: 'task', triggers: ['create'], url: 'https://example.com/hook' }, }); expect(result.success).toBe(true); expect(metaRows(rows).length).toBe(1); @@ -496,9 +497,10 @@ describe('code-only metadata types are refused on every kernel (#5086)', () => { item: { name: 'rc3_receipt_view', object: 'task', events: ['beforeUpdate'] }, }, { - type: 'theme', // no static registry entry (plugin-registered) - // [#10194] spec-valid body — theme resolves a schema now. - item: { name: 'rc3_receipt_view', label: 'Receipt', colors: { primary: '#3b82f6' } }, + type: 'webhook', // no static registry entry (plugin-registered) + // [#6245] spec-valid body — webhook resolves a schema. + // (`theme` was the specimen until #10485 retired that kind.) + item: { name: 'rc3_receipt_view', label: 'Receipt', object: 'task', triggers: ['create'], url: 'https://example.com/hook' }, }, ]; diff --git a/packages/objectql/src/protocol-meta.test.ts b/packages/objectql/src/protocol-meta.test.ts index 2b8602e421..38ebf1a4a4 100644 --- a/packages/objectql/src/protocol-meta.test.ts +++ b/packages/objectql/src/protocol-meta.test.ts @@ -1555,6 +1555,13 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => { // to take one from — #6242 row 2), so it carries the pure // no-schema fall-through control below. Each newly-bound type's own // behaviour, door and 422 both, is pinned in the tests below. + // + // [#10485] `theme` then left the CONTRACT ITSELF (ADR-0049 — the + // `themes` carrier and `ThemeSchema` retired; the `themes: 'theme'` + // fold left `PLURAL_TO_SINGULAR`), so it is no longer a URL-map-only + // kind at all: both halves of its old pair now earn the #8421 + // unrecognised refusal, pinned once below; `webhook` carries the + // two-halves door pin for the class. // ─────────────────────────────────────────────────────────────── it('accepts brand-new plugin-registered type (no static registry entry)', async () => { @@ -1755,16 +1762,23 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => { // change that quietly CLOSED the door fails the "accepts" half. // ─────────────────────────────────────────────────────────────── - it('accepts a spec-valid `theme` item (write door unchanged by the schema binding)', async () => { + // [#10485] `theme` left this pair: the carrier retired out of the + // spelling contract, so BOTH halves now earn the #8421 unrecognised + // refusal before any schema is consulted — pinned once below. The + // still-bound `webhook` door keeps the two-halves pin alive for the + // #6245 class. + it('accepts a spec-valid `webhook` item (write door unchanged by the schema binding)', async () => { mockEngine.findOne.mockResolvedValue(null); const result = await scoped.saveMetaItem({ - type: 'theme', - name: 'my_theme', + type: 'webhook', + name: 'my_hook', item: { - name: 'my_theme', - label: 'My Theme', - colors: { primary: '#3b82f6' }, + name: 'my_hook', + label: 'My Hook', + object: 'task', + triggers: ['create'], + url: 'https://example.com/hook', }, organizationId: 'org_alpha', }); @@ -1772,20 +1786,17 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => { expect(result.success).toBe(true); }); - it('refuses a spec-INVALID `theme` item with 422 instead of storing it unvalidated', async () => { + it('refuses a spec-INVALID `webhook` item with 422 instead of storing it unvalidated', async () => { mockEngine.findOne.mockResolvedValue(null); - // The exact body the old "plugin-registered types" case above used - // to save with `success: true`: `tokens` is an ALIAS of - // `customVars` (so the strict surface names the real key), and the - // required `colors` block is missing. Stored verbatim, this is the - // theme that fails at RENDER — the console's own styling surface — - // with nothing at the write point to say so. + // Required keys missing (`object`, `triggers`, `url`): stored + // verbatim this is the webhook that never fires, with nothing at + // the write point to say so. await expect( scoped.saveMetaItem({ - type: 'theme', - name: 'my_theme', - item: { name: 'my_theme', label: 'Test', tokens: {} }, + type: 'webhook', + name: 'my_hook', + item: { name: 'my_hook', label: 'Test' }, organizationId: 'org_alpha', }), ).rejects.toMatchObject({ @@ -1794,6 +1805,22 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => { }); }); + it('[#10485] refuses `theme` outright — the retired kind is no longer addressable', async () => { + mockEngine.findOne.mockResolvedValue(null); + + await expect( + scoped.saveMetaItem({ + type: 'theme', + name: 'my_theme', + item: { name: 'my_theme', label: 'My Theme', colors: { primary: '#3b82f6' } }, + organizationId: 'org_alpha', + }), + ).rejects.toMatchObject({ + code: 'INVALID_REQUEST', + status: 400, + }); + }); + it('accepts a spec-valid `analytics_cube` item (write door unchanged by the schema binding)', async () => { mockEngine.findOne.mockResolvedValue(null); diff --git a/packages/rest/src/meta-unknown-type-read-refusal.test.ts b/packages/rest/src/meta-unknown-type-read-refusal.test.ts index a07c59eb8e..a76520234e 100644 --- a/packages/rest/src/meta-unknown-type-read-refusal.test.ts +++ b/packages/rest/src/meta-unknown-type-read-refusal.test.ts @@ -18,8 +18,10 @@ // regression than the bug. Three populations therefore have to keep answering // `200` with an empty collection, and each is here for a different reason: // -// * types in the static spelling contract (`sharing_rule`, `theme`, -// `objects`, `api`) — declared, addressable, frequently empty; +// * types in the static spelling contract (`sharing_rule`, `webhook`, +// `objects`, `api`) — declared, addressable, frequently empty +// (`theme` was one of them until #10485 retired its carrier out of the +// contract — it now earns the refusal, pinned below); // * live-only keys an ordinary `registerApp` produces (`data`, `kind`, // `package`, `policy`) — outside the static contract but ENUMERATED by // `GET /meta/types`, which is precisely why #8421 refused to raise the @@ -168,7 +170,7 @@ describe('[#9488] the read door and the write door agree on one invented name', describe('[#9488] a type that EXISTS and has no items still answers 200 with an empty collection', () => { // The static spelling contract's own members — declared and addressable, // whether or not this deployment holds a single item of them. - it.each(['sharing_rule', 'sharingRules', 'theme', 'themes', 'objects', 'object', 'api', 'external_catalogs'])( + it.each(['sharing_rule', 'sharingRules', 'webhook', 'webhooks', 'objects', 'object', 'api', 'external_catalogs'])( 'declared type %s', async (type) => { const { rest } = setup(); @@ -179,6 +181,21 @@ describe('[#9488] a type that EXISTS and has no items still answers 200 with an }, ); + it.each(['theme', 'themes'])( + '[#10485] retired spelling %s is refused — it left the static contract with its carrier', + async (type) => { + // Until #10485 both spellings answered 200-empty here. The + // retirement removed the `themes: 'theme'` fold, so a read now + // gets the same ADR-0112 refusal an invented name does. + const { rest } = setup(); + + const res = await listType(rest, type); + + expect(res.statusCode).toBe(400); + expect(res.body?.code).toBe('INVALID_REQUEST'); + }, + ); + // The population #8421 named when it REFUSED to raise the static verdict on // the read entries: live `SchemaRegistry` keys an ordinary `registerApp` // produces, which `GET /meta/types` enumerates. Refusing these would answer diff --git a/packages/runtime/src/meta-compound-arity-mint-door.test.ts b/packages/runtime/src/meta-compound-arity-mint-door.test.ts index b0ce3c21ae..a2d0437c0e 100644 --- a/packages/runtime/src/meta-compound-arity-mint-door.test.ts +++ b/packages/runtime/src/meta-compound-arity-mint-door.test.ts @@ -254,16 +254,17 @@ describe('#8421 — the compound `/meta` arity is not a metadata-type claim', () it('CONTROL — a recognised type at the simple arity is unaffected', async () => { const { engine, dispatcher } = makeStack(); - // [#10194] spec-valid body — `theme` resolves a schema through - // UNREGISTERED_KIND_SCHEMAS now, and this control measures the ARITY - // door, so a malformed body would 422 and misread it. + // [#6245] spec-valid body — `webhook` resolves a schema through + // UNREGISTERED_KIND_SCHEMAS, and this control measures the ARITY + // door, so a malformed body would 422 and misread it. (`theme` was + // the specimen until #10485 retired that kind.) const res = responseOf(await dispatcher.handleMetadata( - '/theme/midnight', ctx(), 'PUT', - { name: 'midnight', label: 'Midnight', colors: { primary: '#3b82f6' } }, + '/webhook/midnight_hook', ctx(), 'PUT', + { name: 'midnight_hook', label: 'Midnight', object: 'task', triggers: ['create'], url: 'https://example.com/hook' }, )); expect(res.status).toBe(200); - expect(metaRow(engine, 'theme', 'midnight')).toBeDefined(); + expect(metaRow(engine, 'webhook', 'midnight_hook')).toBeDefined(); }); it('CONTROL — the capability gate still fires first on the compound form', async () => { diff --git a/packages/runtime/src/meta-field-overlay-lock.test.ts b/packages/runtime/src/meta-field-overlay-lock.test.ts index 75d62dbc49..590d1c40f0 100644 --- a/packages/runtime/src/meta-field-overlay-lock.test.ts +++ b/packages/runtime/src/meta-field-overlay-lock.test.ts @@ -696,26 +696,28 @@ describe('#7743 — PUT /meta/field/. honours the registry overla it('#7894 POSITIVE CONTROL — a plugin-registered runtime type is still permitted', async () => { const { engine, dispatcher } = makeStack(); - // `theme` has no `DEFAULT_METADATA_TYPE_REGISTRY` entry at all. - // [#10194] spec-valid bodies — `theme` resolves a schema through - // UNREGISTERED_KIND_SCHEMAS now, and this control measures the #7894 + // `webhook` has no `DEFAULT_METADATA_TYPE_REGISTRY` entry at all. + // [#6245] spec-valid bodies — `webhook` resolves a schema through + // UNREGISTERED_KIND_SCHEMAS, and this control measures the #7894 // PERMISSION verdict, so a malformed body would 422 and misread it. + // (`theme` was the specimen until #10485 retired that kind out of the + // spelling contract.) const singular = responseOf(await dispatcher.handleMetadata( - '/theme/midnight', ctx(), 'PUT', - { name: 'midnight', label: 'Midnight', colors: { primary: '#3b82f6' } }, + '/webhook/midnight_hook', ctx(), 'PUT', + { name: 'midnight_hook', label: 'Midnight', object: 'task', triggers: ['create'], url: 'https://example.com/hook' }, )); expect(singular.status).toBe(200); - expect(metaRow(engine, 'theme', 'midnight')).toBeDefined(); + expect(metaRow(engine, 'webhook', 'midnight_hook')).toBeDefined(); // …and via its plural spelling, which the URL map carries from the // manifest map's limb — still one namespace, the singular one. const plural = responseOf(await dispatcher.handleMetadata( - '/themes/twilight', ctx(), 'PUT', - { name: 'twilight', label: 'Twilight', colors: { primary: '#3b82f6' } }, + '/webhooks/twilight_hook', ctx(), 'PUT', + { name: 'twilight_hook', label: 'Twilight', object: 'task', triggers: ['create'], url: 'https://example.com/hook' }, )); expect(plural.status).toBe(200); - expect(metaRow(engine, 'theme', 'twilight')).toBeDefined(); - expect(metaRow(engine, 'themes', 'twilight')).toBeUndefined(); + expect(metaRow(engine, 'webhook', 'twilight_hook')).toBeDefined(); + expect(metaRow(engine, 'webhooks', 'twilight_hook')).toBeUndefined(); }); it('#7894 POSITIVE CONTROL — the #7894 refusal still cannot reach a never-heard-of kind (#8421 CHANGED the boundary)', async () => { @@ -738,7 +740,7 @@ describe('#7743 — PUT /meta/field/. honours the registry overla // `@objectstack/metadata-protocol` for the full record. // // The discriminating control for that narrowing is the case directly - // ABOVE, which must stay green: `theme` has no registry entry either + // ABOVE, which must stay green: `webhook` has no registry entry either // and is still minted, because it IS in the static contract. If a // change ever breaks both, the narrowing stopped being narrow. const { engine, dispatcher } = makeStack(); diff --git a/packages/runtime/src/meta-write-org-scope.test.ts b/packages/runtime/src/meta-write-org-scope.test.ts index e012e4c912..1691246dee 100644 --- a/packages/runtime/src/meta-write-org-scope.test.ts +++ b/packages/runtime/src/meta-write-org-scope.test.ts @@ -327,8 +327,9 @@ describe('#7018 — the registry decides whether a metadata write carries the se expect(declaresOrgOverride('views')).toBe(declaresOrgOverride('view')); expect(declaresOrgOverride('flows')).toBe(declaresOrgOverride('flow')); // A runtime-registered type with no registry entry has no per-org read - // channel either, so it is env-wide too. - expect(declaresOrgOverride('theme')).toBe(false); + // channel either, so it is env-wide too. (`webhook` took this slot + // from `theme` at #10485 — the retired kind left the contract.) + expect(declaresOrgOverride('webhook')).toBe(false); // No active org in, no org out — for every type. expect(organizationIdForMetaWrite('view', undefined)).toBeUndefined(); }); diff --git a/packages/spec/PROTOCOL_MAP.md b/packages/spec/PROTOCOL_MAP.md index c63d35bff4..b7e14ba4a3 100644 --- a/packages/spec/PROTOCOL_MAP.md +++ b/packages/spec/PROTOCOL_MAP.md @@ -56,7 +56,6 @@ This document serves as the **Grand Map** of the ObjectStack specification. It l | [`chart.zod.ts`](src/ui/chart.zod.ts) | | **Charts**. Visualization configurations (Bar, Pie, Line, etc.). | | [`widget.zod.ts`](src/ui/widget.zod.ts) | | **Widgets**. Reusable UI components for dashboards and pages. | | [`component.zod.ts`](src/ui/component.zod.ts) | | **Components**. Low-level component definitions. | -| [`theme.zod.ts`](src/ui/theme.zod.ts) | | **Theming**. Color palettes and visual styling rules. | --- diff --git a/packages/spec/src/kernel/metadata-type-schemas.test.ts b/packages/spec/src/kernel/metadata-type-schemas.test.ts index cfc7f7c646..98398db71f 100644 --- a/packages/spec/src/kernel/metadata-type-schemas.test.ts +++ b/packages/spec/src/kernel/metadata-type-schemas.test.ts @@ -367,6 +367,8 @@ describe('#6931 — the envelope invariant also covers UNREGISTERED_KIND_SCHEMAS * sentence was prose until #10194 found its converse failure mode — two stack * collections (`themes`, `analyticsCubes`) with strict schemas and NO map * entry, so the divergence the sentence forbids simply lived outside the map. + * (#10485 later retired the `themes` carrier and `ThemeSchema` whole, and the + * `theme` binding left the map with them — the row below went too.) * * Two halves, both load-bearing: * @@ -386,7 +388,7 @@ const STACK_COLLECTION_OF: Record = { webhook: 'webhooks', connector: 'connectors', sharing_rule: 'sharingRules', - theme: 'themes', + // theme: 'themes' — retired at #10485 (ADR-0049). analytics_cube: 'analyticsCubes', }; diff --git a/packages/spec/src/shared/metadata-collection.test.ts b/packages/spec/src/shared/metadata-collection.test.ts index 17d1e439d4..9339009c64 100644 --- a/packages/spec/src/shared/metadata-collection.test.ts +++ b/packages/spec/src/shared/metadata-collection.test.ts @@ -247,7 +247,7 @@ describe('MAP_SUPPORTED_FIELDS', () => { expect(MAP_SUPPORTED_FIELDS).toContain('dashboards'); expect(MAP_SUPPORTED_FIELDS).toContain('reports'); expect(MAP_SUPPORTED_FIELDS).toContain('actions'); - expect(MAP_SUPPORTED_FIELDS).toContain('themes'); + // 'themes' left the list at #10485 (ADR-0049 — carrier retired). expect(MAP_SUPPORTED_FIELDS).toContain('flows'); expect(MAP_SUPPORTED_FIELDS).toContain('positions'); expect(MAP_SUPPORTED_FIELDS).toContain('permissions'); diff --git a/packages/spec/src/shared/metadata-url-spelling.test.ts b/packages/spec/src/shared/metadata-url-spelling.test.ts index 475d77e67e..0d020e7efe 100644 --- a/packages/spec/src/shared/metadata-url-spelling.test.ts +++ b/packages/spec/src/shared/metadata-url-spelling.test.ts @@ -46,9 +46,10 @@ describe('#7894 INVARIANT 1 — no spelling that worked before may stop working' it('folds every manifest spelling to exactly the singular it folded to before', () => { // The manifest map is the complete population of spellings that resolved at // the `/meta` boundary before this change, so quantifying over it IS the - // non-breaking proof. It includes six that name PLUGIN kinds with no static - // registry entry (`themes`, `webhooks`, `connectors`, `sharingRules`, - // `ragPipelines`, `analyticsCubes`) — a purely registry-derived map would + // non-breaking proof. It includes five that name PLUGIN kinds with no static + // registry entry (`webhooks`, `connectors`, `sharingRules`, + // `ragPipelines`, `analyticsCubes`; `themes` was the sixth until #10485 + // retired its carrier out of the map) — a purely registry-derived map would // have dropped those, which is why the derived limb is unioned rather than // substituted. for (const [plural, singular] of Object.entries(PLURAL_TO_SINGULAR)) { @@ -62,14 +63,17 @@ describe('#7894 INVARIANT 1 — no spelling that worked before may stop working' } }); - it('carries the six plugin-kind spellings that no registry derivation could produce', () => { + it('carries the five plugin-kind spellings that no registry derivation could produce', () => { // Named explicitly because losing them is the specific regression the // union-vs-pure-derivation choice was made to avoid, and a reader needs to // see the population rather than infer it. const registryTypes = new Set(DEFAULT_METADATA_TYPE_REGISTRY.map((e) => e.type)); const pluginOnly = Object.entries(PLURAL_TO_SINGULAR).filter(([, s]) => !registryTypes.has(s)); + // `themes` left this list at #10485 (ADR-0049 — the carrier retired, so + // `/meta/theme` now earns `unrecognisedMetaTypeRefusal`'s verdict; pinned + // in the #8421 block at the bottom of this file). expect(pluginOnly.map(([p]) => p).sort()).toEqual( - ['analyticsCubes', 'connectors', 'ragPipelines', 'sharingRules', 'themes', 'webhooks'], + ['analyticsCubes', 'connectors', 'ragPipelines', 'sharingRules', 'webhooks'], ); for (const [plural, singular] of pluginOnly) { expect(canonicalMetaUrlType(plural)).toBe(singular); @@ -158,11 +162,15 @@ describe('#7894 — the refusal limb is narrow by construction', () => { // misspellings of DECLARED types and none of these is one. Read it as a // statement about `metaUrlSpellingRefusal`, not about the whole boundary: // `saveMetaItem` now also consults `unrecognisedMetaTypeRefusal`, under - // which the six mapped kinds below stay writable and the six unmapped + // which the five mapped kinds below stay writable and the six unmapped // names no longer are. That narrowing is pinned, deliberately visible, in - // the `#8421` block at the bottom of this file. + // the `#8421` block at the bottom of this file. (`theme` moved from this + // list to the refused side at #10485 — its carrier retired out of the map, + // so it is no longer "a spelling that worked before" for this verdict: + // `metaUrlSpellingRefusal` still answers null for it, but the #8421 + // verdict now refuses it, pinned below.) for (const kind of [ - 'theme', 'sharing_rule', 'webhook', 'rag_pipeline', 'analytics_cube', 'connector', + 'sharing_rule', 'webhook', 'rag_pipeline', 'analytics_cube', 'connector', 'my_plugin_kind', 'address', 'status', 'kudos', 'analysis', 'series', ]) { expect(metaUrlSpellingRefusal(kind), `${kind} must not be refused`).toBeNull(); @@ -216,13 +224,14 @@ describe('#8421 — the second verdict: not a metadata type AT ALL', () => { it('accepts every manifest spelling AND the singular each one folds to', () => { // The direction that matters most, and the one a registry-quantified - // refusal would get wrong: six of these singulars — `theme`, `webhook`, + // refusal would get wrong: five of these singulars — `webhook`, // `connector`, `sharing_rule`, `analytics_cube`, `rag_pipeline` — are // PLUGIN kinds with no static registry entry at all. Refusing them would - // break `PUT /meta/theme/dark`, the exact operation the plugin path exists - // to serve, which is the failure #8421's measurement disqualified option C - // for. Accepting the singular is not decoration: `themes` folds to `theme`, - // and a boundary that refused the fold's own output would be incoherent. + // break `PUT /meta/webhook/stripe`, the exact operation the plugin path + // exists to serve, which is the failure #8421's measurement disqualified + // option C for. Accepting the singular is not decoration: `webhooks` folds + // to `webhook`, and a boundary that refused the fold's own output would be + // incoherent. for (const [plural, singular] of Object.entries(PLURAL_TO_SINGULAR)) { expect(unrecognisedMetaTypeRefusal(plural), `${plural} works today`).toBeNull(); expect( @@ -232,16 +241,26 @@ describe('#8421 — the second verdict: not a metadata type AT ALL', () => { } }); - it('carries the six plugin kinds by NAME, since quantification hides them', () => { + it('carries the five plugin kinds by NAME, since quantification hides them', () => { const declared = new Set(DEFAULT_METADATA_TYPE_REGISTRY.map((e) => e.type)); for (const kind of [ - 'analytics_cube', 'connector', 'rag_pipeline', 'sharing_rule', 'theme', 'webhook', + 'analytics_cube', 'connector', 'rag_pipeline', 'sharing_rule', 'webhook', ]) { expect(declared.has(kind), `${kind} must NOT be in the static registry`).toBe(false); expect(unrecognisedMetaTypeRefusal(kind), `${kind} must stay accepted anyway`).toBeNull(); } }); + it('[#10485] `theme` left the contract with its carrier — refused by name', () => { + // The retired kind's whole exit path: the `themes: 'theme'` fold left + // `PLURAL_TO_SINGULAR`, so both spellings now earn the #8421 verdict + // instead of the pre-#10194 store-anything branch. + for (const spelling of ['theme', 'themes']) { + const verdict = unrecognisedMetaTypeRefusal(spelling); + expect(verdict, `${spelling} must be refused`).not.toBeNull(); + } + }); + it('refuses the card coordinates — a name the contract does not carry', () => { for (const type of ['fieldz', 'objectt', 'viewz', 'nonsense_type']) { expect(unrecognisedMetaTypeRefusal(type)).toEqual({ type }); diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index 6bee49261c..38a7ecf190 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -265,7 +265,7 @@ import type * as M167 from './ui/view.zod.js'; import type * as M170 from './ui/component.zod.js'; // --------------------------------------------------------------------------- -// 839 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. +// 834 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. // // That number is machine-checked, not hand-kept. The runtime companion at the // bottom of this file recomputes the pin count from the source and asserts that @@ -1658,7 +1658,7 @@ describe('ADR-0122 type-alias convention', () => { // this title and the section header above the pin list — are now asserted // against the recomputed count below, so neither can go stale without a red // test naming it. - it('still declares all 839 isomorphic pins', () => { + it('still declares all 834 isomorphic pins', () => { // The truth of each pin is proved by tsc, not here — an `Assert>` // that stops holding is a compile error with the alias named. What tsc // cannot notice is a pin that was DELETED: removing the assertion removes @@ -1925,9 +1925,14 @@ describe('ADR-0122 type-alias convention', () => { // schemas or converted aliases to `Parsed` pairs; this one moved the // input/output identity into the annotation itself, where both halves // spell the same type. + // + // 839 -> 834 is #10485 — `Iso700`-`Iso704` DELETED with `ui/theme.zod.ts` + // (ADR-0049 retirement of the whole theme authoring surface): the five + // schemas they pinned no longer exist, so there is nothing left to exempt. + // The Iso numbers are positional and stay vacant. const self = readFileSync(fileURLToPath(import.meta.url), 'utf8'); const pins = self.match(/^export type Iso\d+ = Assert Date: Fri, 21 Aug 2026 08:42:48 +0000 Subject: [PATCH 3/5] =?UTF-8?q?docs(audits):=20strictness=20ledger=20?= =?UTF-8?q?=E2=80=94=20theme.zod.ts=20row=20becomes=20a=20retirement=20par?= =?UTF-8?q?agraph=20(#10485)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw --- docs/audits/2026-07-unknown-key-strictness-ledger.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.md b/docs/audits/2026-07-unknown-key-strictness-ledger.md index 1305f143e1..e651f83f3d 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.md @@ -648,7 +648,6 @@ sites left to be a verdict about. | `view.zod.ts` | authorable | partially strict (ADR-0089); long tail of sub-blocks. `bulkActionDefs` left this file in #4457 — see the row below | | `bulk-action.zod.ts` | authorable | **strict as of #4457** — `BulkActionDefSchema` (the def itself). It was `z.array(z.record(z.string(), z.any()))` inline in `view.zod.ts`: a selection-bar button with **no shape at all**, so `opeartion` / `excution: 'aggregate'` parsed and shipped as a button that ran the default behaviour. Its two other sites are `BulkActionParamSchema` and that param's `options` entry, both deliberately **open** and both now `.passthrough()` — the param because objectui's `BulkActionParam` declares a `[key: string]: unknown` catch-all for widget config (min/max/step/format), so passthrough is the honest mirror and strictness would reject valid config (same call as `dashboard.zod.ts`'s widget `config`); the OPTION ENTRY on separate measured evidence, since its objectui type is closed and only the runtime path is open — `bulkParamToField` spreads each entry (`plugin-grid/src/components/bulkParamToField.ts:131`) into `SelectOptionMetadata` (`types/src/field-types.ts:288`), which declares and reads `color` / `icon` / `disabled` / `visibleWhen`. **This row said "both deliberately open" while only the parent was `passthrough`** — one intent, two postures, caught by the 2026-08-03 re-measure and closed by the ruling's verdict A (make the code match the prose). The lesson is the campaign's own: prose in this ledger is not a posture reading, which is why the remaining-strip map is gated and this column is not. The def also refuses the combinations the executor never reads (`patch` outside an update, `execution` outside a custom, `batchSize` on an aggregate) and a hand-written `actionDef`, which is renderer-attached | | `component.zod.ts` | ~~authorable (p)~~ ~~no gate~~ **authorable (gate wired at #5068)** | **strict as of #4001 batch A — 0 strip sites remain (all 31).** The `(gate wired at #5068)` qualifier is spent in its turn: the parse it wired is now a parse of CLOSED shapes, so an undeclared prop is reported by `safeParse` as `unrecognized_keys` rather than reconstructed by the walker — same rule id, same warning tier, one fewer moving part. What batch A did NOT do is the half this row keeps insisting on: the carrier is still `z.record(z.string(), z.unknown())` (direction B stays declined), and the storage path still parses no props at all (#4463), so this closes the AUTHORING door and nothing else. Three renderer-honoured keys were DECLARED in the same pass rather than rejected — `page:header` `maxVisible`/`mobileMaxVisible`, `page:tabs.alwaysShowStrip`, `record:details` `inlineEdit`/`showHeader` — each read by objectui through `schema?.X ?? schema?.properties?.X` with its own comment inviting authors, which is the #5611/#5775/#6276 rule applied a fourth time (enumerate by the RENDERER'S read pattern, not by the key list a previous ruling quoted). The evidence below is unchanged and still the reason the file took this long. **no parse anywhere (measured, #4001 批 17; the parse landed at #5068 — see the end of this cell)** — the `(p)` resolved NEGATIVE, and this is the campaign's largest single reclassification. The standing warning said to verify objectui's React-prop open slots first; doing so found the question was moot one level up. **The carrier is live but it is an open bag**: `PageComponentSchema.properties` is `z.record(z.string(), z.unknown())`, and although `PageComponentSchema` has been `.strict()` since ADR-0089 D3a, **strictness does not recurse** — it closes the component node's own keys and leaves everything under `properties` unchecked. Nothing dispatches `ComponentPropsMap` by `type`. Three measurements on 2026-08-04, controls green in the same run: (1) a BFS from all 24 metadata-type roots plus `ObjectStackSchema`, over a 6899-node closure built with `build-schemas.ts`'s own `zodChildSchemas`/`zodShapeOf` (the #4650 walk), returns **UNREACHABLE for all 52 targets** (21 exported schemas + every one of `ComponentPropsMap`'s 31 entries), while `PageSchema`/`PageComponentSchema`/`PageRegionSchema`/`ThemeSchema`/`ChartConfigSchema`/`ResponsiveConfigSchema` all resolve `root-graph` and 批 13's no-door shapes stay unreachable — the walk stops dead at `properties`. ⚠️ The #5056 bridge defect does not touch this row: it makes the derived-clone bridge report dead shapes as REACHABLE, the opposite direction, and nothing here rests on that bridge — all six positive controls resolve `root-graph` and all 52 targets miss BOTH `root-graph` and `derived-clone`; (2) across `objectstack`, `objectui` and `cloud`, every `.parse()`/`.safeParse()` on anything in this file is inside the file's own unit tests — objectui mirrors the props as hand-written React interfaces and imports only the inferred TYPES, `cloud` references none, and `react-blocks.ts` uses `Object.keys(ComponentPropsMap)` for type NAMES only (its `REACT_BLOCKS[].schema` entries all point at view/chart schemas); (3) empirically through the live door — `definePage()` IS `PageSchema.parse()` — an undeclared key written inside `components[].properties` parses clean and is RETAINED on 10/10 example-corpus pages, while the same key one level out is rejected on 10/10 (the negative control that makes the first number mean anything). ⚠️ **`no gate`, not `no door`** — the vocabulary is ALIVE and must not be retired: objectui's `SchemaRenderer` hoists `properties` onto the node and spreads every key not on its fixed deny-list straight into the React component, so a misspelled key is neither rejected nor dropped — it reaches the renderer and is ignored there, the ADR-0078 failure mode one layer below where this ratchet reaches. That IS the #4909 open-slot shape, but `.passthrough()` would be exactly as vacuous as `.strict()` on a schema nothing parses, so no posture change was made. The fix is to wire the parse at the carrier's own gate — a `packages/lint`/carrier change, filed as **#5068**, which also records the two constraints that stop it being a drive-by: `type` is an open union (`z.union([PageComponentType, z.string()])`, so `record:line_items`-style unregistered types are authored in the wild) and real pages already author shapes these schemas do not declare (`record:details` `sections[].fields[]`/`hideFields[]`, the record picker's `labelField` — `packages/lint/src/validate-page-field-bindings.ts` has documented the untyped bag all along). **Do not reschedule this as strictness work** — that is what the `(p)` was for, and it has been answered. ✅ **#5068 answered it in turn: the parse is wired (lint side, warning level) and the class is `authorable` again** — the strip-table row below carries the full flip, including the three things it did not do. One correction belongs here rather than there, because this row is where the wrong expectation was written down: the pin in `component.test.ts` said its carrier assertion *"goes red the day `properties` gets a typed dispatch"*, which assumed direction B. The dispatch that landed is direction A — on `packages/lint`'s authoring gate — so the assertion is GREEN after the fix, and it was measured that way rather than predicted. The pin now says which dispatch landed and what a future red there would mean (the carrier reshaped: a protocol change, not a lint one). Recorded in three places (file header, `component.test.ts` pin, this row) | -| `theme.zod.ts` | authorable | **strict as of #4001 批 15** — all 14 sites. The `(p)` resolved to authorable on two doors, both measured: `stack.zod.ts` declares `themes: z.array(ThemeSchema)` (so `defineStack()` parses every theme on boot and on `objectstack build`), and `defineTheme()` parses one directly. A BFS from all 24 metadata-type roots plus `ObjectStackSchema` reaches every schema in the file, with `PageSchema`/`DashboardSchema`/`ReportSchema`/`WebhookSchema`/`StateMachineSchema` passing as positive controls and 批 13's no-door shapes failing as negative controls **in the same run**. Note what is NOT claimed: `theme` is deliberately absent from `BUILTIN_METADATA_TYPE_SCHEMAS`, so a stored theme row is not validated by the metadata REST door — the gate is the authoring one, and the file says so rather than implying reach it lacks. **The `passthrough` question was asked per BLOCK, not per file**, and the answer split: objectui's `ThemeEngine` reads `colors`/`borderRadius`/`shadows`/`typography.fontFamily` through FIXED maps (an extra key is read by nothing, ever), but spreads `fontSize`/`fontWeight`/`lineHeight`/`letterSpacing`/`duration`/`timing`/`zIndex` with `Object.entries` into `--font-size-` … — the #4909 open shape at the runtime. Closed anyway, on two measurements: `.strip` already discarded those extras before the engine saw them (so no author depends on the openness and nothing the renderer receives changes), and `customVars` is a DECLARED escape hatch that emits an arbitrary CSS custom property by name, so closing the token scales removes no capability and only removes a second, undocumented way to spell one — the way whose typos are indistinguishable from intent. Curation is measured throughout: the shadcn vocabulary (`card`→`surface`, `foreground`→`text`, `destructive`→`error`) comes from objectui's own `COLOR_TO_CSS_MAP`, which RENAMES every palette key on the way out; `md`→`base` on `fontSize` and `base`→`normal` on `fontWeight` are a same-file scale disagreement (`borderRadius`/`shadows` declare `md`, `fontSize` does not); `radius`→`base` because `base` is emitted as the bare `--radius`, the one radius variable objectui's CSS actually reads; and `easeIn`→`ease_in` because `animation.timing` is the file's single snake_case vocabulary, so the camelCase spelling is an author obeying AGENTS.md #3 rather than making a typo. The eight #3494 removals get one distinct tombstone each. ⚠️ **Two of those tombstones deliberately prescribe NO replacement slot**: `touchTarget`/`keyboardNavigation` read like they should point at `ui/touch.zod.ts`/`ui/keyboard.zod.ts`, which 批 13 measured as having no carrier at all — prescribing them would walk an author out of a loud rejection into a silent one, the ledger's finding 7. **#4988 then retired both modules outright**, so the two tombstones' refusal to name a replacement is now the only correct wording available: had they pointed at `ui/touch.zod.ts` / `ui/keyboard.zod.ts`, that prescription would today name a deleted file — finding 7 with an extra major on top. ⚠️ **Separately filed — and ANSWERED at #5021, which is why this row's site count fell 14 → 6.** 批 15 recorded that `--font-size-*`, `--font-weight-*`, `--line-height-*`, `--letter-spacing-*`, `--z-*`, `--duration-*`, `--timing-*`, `--font-heading` and `--font-mono` have ZERO first-party consumers (only the colour vars, `--radius*`, `--shadow*` and `--font-sans` are read), and refused to act on it inside a strictness batch: that is ADR-0049 liveness, not unknown keys, and the two must not be run together — strictness makes a dropped key loud, it cannot make a slot live. The refusal was correct and the separation is what made the follow-up answerable. #5021 re-measured against objectui `main` (2026-08-04) with `--font-sans`/`--radius`/`--shadow`/`--primary` as positive controls **in the same run**, the maintainer ruled RETIRE over both alternatives (wire consumers / bless as a public token surface — the latter rejected as a stability promise attached to a slot the platform's own UI ignores, the #4583 shape), and `typography.fontSize`/`.fontWeight`/`.lineHeight`/`.letterSpacing`, `typography.fontFamily.heading`/`.mono`, `animation` and `zIndex` are now `retiredKey()` tombstones prescribing `customVars`. **Note what this row's arithmetic does NOT say**: the eight sites left `ui/` from the `strict` column (120 → 112), and `strip` is unchanged at 75 — a retirement removes closed doors, so it cannot move this ratchet's open-site debt in either direction. The two campaigns stayed disjoint to the end. ⚠️ The prescription is `customVars` **because it was measured live**, not because it is the nearest-looking slot: the engine emits each entry as `--: ` verbatim, so every retired variable is reproducible byte for byte and the retirement removes no capability — the distinction from `touchTarget`/`keyboardNavigation` two sentences up, which got NO replacement precisely because theirs would have been a guess. The five aliases pointing at the retired keys (`animations`/`motion`/`transitions` → `animation`, `layers`/`stacking` → `zIndex`) and the seven pointing into the retired typography scales were **deleted with their targets**, not re-pointed — leaving them would answer an author with "did you mean `zIndex`?" and then reject `zIndex`, finding 7's exact shape, and this file has now signposted that failure mode three times | | `app.zod.ts` | authorable | **strict as of #4001 PR B** — `AppSchema` + branding / area / context-selector / contribution, and the nav-item union converted to `z.discriminatedUnion('type', …)` (the union-error question, settled empirically: matched-branch-only errors, exact recursive paths, `toJSONSchema` clean). Per-target `params` stay open. PR A (#4142) tombstoned the seven audit-dead keys first | | `dashboard.zod.ts` | authorable | **strict as of #4001 批 14 — 0 strip sites remain.** `DashboardWidgetSchema` has been strict since the ADR-0021 cutover; 批 14 closed the two NESTED holes inside it (`compareTo`'s object arm, `layout`), the same strict-shell-over-strip-children silhouette 批 13 found on `page.components[]`. `DashboardWidgetOptionsSchema` stays `passthrough` **deliberately** (renderer escape hatch) and the `responsive` tombstone (#4876) is untouched. ⚠️ **The `compareTo` union caveat this row carried is RESOLVED, and it is the one entry in this table whose limit was dissolved rather than worked around.** 批 14 recorded that `compareTo` was a UNION, so its curated prescription was produced but never delivered — `zodIssuesToFields` maps only top-level issues and a failed union collapses to a bare `Invalid input` (#5014) — with the rejection itself unaffected. **#5011 removed the union**: the slot converged onto the analytics executor's own contract, `{ kind, dimension? }`, a plain strict object whose message IS top-level. The reason was not the message, it was worse — all three declared arms were broken on the ADR-0021 dataset path (the two strings silently dropped by the renderer, `{ offset }` throwing `compareTo requires a timeDimension "undefined"`), while all three worked on the legacy inline path: same key, two fates, the failing one blessed. The union-free shape is the design benefit, pinned in `dashboard-compareto.test.ts` so it cannot silently return. **#5014 still binds every OTHER curated message this campaign has put inside a union arm** — this row is one slot's correction, not the finding's retraction. ⚠️ **#5010 retired four more widget keys and moved this row's posture by nothing, which is the point.** The `#4956` drill gave `DashboardWidgetSchema`'s 22 widget-level keys their first per-key verdicts and found six dead; `actionUrl`/`actionType`/`actionIcon` (a per-widget action BUTTON no renderer in either repo has ever drawn — all 14 `actionUrl` reads in `DashboardRenderer` are scoped to `header.actions[]`) and `aria` (ARIA attributes that never reached the DOM — the dashboard-level `aria` the #3896 sweep removed, one level down) are now `retiredKey` tombstones beside `responsive`. **Strip sites remain 0 and the strictness verdict is untouched**, because a retirement is ADR-0049 work and this ratchet is not: closing a door makes a *dropped* key loud, it cannot make a *declared* one live — the same boundary `theme.zod.ts` records two rows up, met here from the other side. The removal also settled a second-order cost the strictness campaign could never have reached: `packages/lint`'s dashboard action-ref rule enforced ERROR-severity reference integrity on `widgets[].actionUrl`, its docblock calling the key "the per-widget button" and claiming to mirror a runtime dispatch that does not exist, so an author could FAIL A BUILD because a control that cannot render pointed at an action that also did not — an enforcement gate sustaining the very false affordance ADR-0049 wrote it to delete. That widget branch is gone, pinned. ⚠️ **`colorVariant`, the fifth dead key, is deliberately NOT retired here and this row must not be read as closing it**: the rewrite target the #4956 triage assumed (`options.colorVariant`) measured dead too — `options` only reaches a renderer through `componentSchema` on the INLINE path, and `dataset` is required on this schema, so every spec-authorable widget is dataset-bound and renders through `DatasetWidget`, which has no colour affordance at all. Moving the key there would relocate 16 authored sites from one dead slot to another and mint a second inert key. Returned for adjudication; `chartConfig`'s dashboard-face inertness (11 of 12 keys, #5175) is the same shape on the neighbouring slot | | `widget.zod.ts` | ~~authorable (p)~~ **no door** | **no authoring door (measured, #4001 批 16)** — the `(p)` resolved NEGATIVE for the whole file, the second such run after 批 13's five. Three independent measurements on 2026-08-04: (1) nothing under `packages/spec/src` imports this module except the `ui/index.ts` barrel, so no schema anywhere declares a carrier key for a widget shape — `field.widget` is a `z.string()` naming a registered *component* and has never referenced `WidgetManifest`; (2) a BFS over the in-memory Zod graph from all 24 metadata-type roots plus `defineStack` (4 766 nodes) reaches none of the six shapes, while `PageSchema` / `ObjectListViewSchema` resolve in the same run, a fresh `z.object` and a deliberate look-alike both resolve unreachable, and a synthetic carrier flips all six to reachable; (3) zero `.parse()` / `.safeParse()` in `objectstack`, `objectui` or `cloud` outside this file's own tests — objectui re-exports the inferred TYPES only and under different names (`RuntimeWidgetManifest` / `FieldWidgetComponentProps`, #4115 / #3161), and a `cloud` code search returns 0 for every symbol against a working index (`"@objectstack/spec"` → 345). ADR-0049 enforce-or-remove is **#5055**. ⚠️ **The campaign's own BFS said REACHABLE on the first run** — a false positive in the derived-clone bridge, filed as **#5056**: zod's `.describe()` returns a clone that SHARES the original `_zod.def`, so `WidgetManifestSchema.name` / `.label` (a described `SnakeCaseIdentifierSchema` / `I18nLabelSchema`) are def-identical to the same leaves on live schemas, and a bridge firing on ANY one shared property links two unrelated shapes. 2 shared keys of 20. The error is one-directional — it can only manufacture a door, i.e. it can only make a batch tighten something dead. Corrected to whole-shape overlap in `ui/door-reachability.testkit.ts` and pinned in `widget.test.ts` ✅ **#5055 ANSWERED the ADR-0049 call, and the answer SPLIT 8/1** (maintainer ruling 2026-08-06; window moved v18 → v17 on 2026-08-07). Eight of the nine sites were REMOVED — `WidgetManifestSchema`, `WidgetLifecycleSchema`, `WidgetEventSchema`, `WidgetPropertySchema` and `WidgetSourceSchema` (3 union branches) — after all three measurements above were re-run on `origin/main` with their controls passing in the same run. Route 3 ("nothing parses it → neither"): no carrier key means no shape for a `retiredKey()` tombstone and no source for a D2 conversion, so the declared record is the D3 `SemanticMigration` `ui-widget-i18n-family-retired` plus `RETIRED_DEFS_BY_MAJOR`. `WidgetManifest.performance`'s own tombstone (#3896) was subsumed by the removal of the shape that carried it. ⚠️ **The NINTH site, `FieldWidgetPropsSchema`, was KEPT — do not finish this file.** Its evidence shape differs and the difference arrived one day before 批 16 measured: it is a REACT PROPS CONTRACT, never authorable (absent from `authorable-surface/` and `json-schema.manifest/` — `onChange` is a `z.function()`), so "zero parse" is its design rather than its defect; and objectui PR #3289 (merged 2026-08-03) renamed `@object-ui/fields`' validation slot onto this contract's `error` with no alias, made the form renderer produce it, and pinned it in `packages/fields/src/__tests__/spec-symbol-batch7.test.ts` as a deliberate tripwire — "the day the spec stops exporting `FieldWidgetProps`, this file stops compiling". Re-verified on objectui `origin/main` 2026-08-07. That is a live cross-repo compile-time consumer, and `tsc` is where a props contract is enforced. So this row's remaining site stays `no door` **and stays**: unreachability is not the retirement trigger for a shape that was never authorable. Pinned bidirectionally in `ui/widget-i18n-retirement.test.ts`. ⚠️ The #5056 fixture moved with the schema: `door-reachability.testkit.test.ts` rebuilds the same 2-of-19 shared-leaf shape locally, so the instrument's regression bound is still measured rather than remembered | @@ -660,6 +659,8 @@ sites left to be a verdict about. | `report.zod.ts` | authorable | **strict as of #4001 批 14 — 0 strip sites remain.** `ReportSchema` was already strict; `ReportSortSchema` and `JoinedReportBlockSchema` were not. The order key is the THIRD spelling of "sort" an author meets (`SortNodeSchema`'s `{field, order}`, the widget's flat `sortBy`/`sortOrder`, this `{by, direction}`), and the mappings run in opposite directions, so none is inferrable. ⚠️ `ReportSchema`'s OWN alias table carries a live false prescription (`filter` → `filters`, a key it also rejects; the real key is `runtimeFilter`) — out of 批 14's scope, filed as #5013 and pinned as a known defect in `strictness-batch14.test.ts` so the list cannot outlive it | | `sharing.zod.ts` | authorable | **Was this ledger's first `split` row — one file, two verdicts — and #5015 resolved the dead half, so the split is now history rather than a live classification.** `SharingConfigSchema` is a **live door** and is all that remains: `FormViewSchema.sharing` carries it, `rest-server.ts` mounts the anonymous form routes on `sharing.allowAnonymous` + `sharing.publicLink`, and both example apps author it (`app-showcase` `inquiry.view.ts`, `app-crm` `lead.view.ts`) — **strict as of #4001 批 14**. `EmbedConfigSchema` was the other verdict, **`no door`**: nothing in the repo so much as named the symbol, BFS-unreachable, zero parse. It was not tightened — *"a precisely-validated dead slot is the more convincing lie"* (#4583) — and the ADR-0049 call filed as #5015 came back **REMOVE** (2026-08-04); the shape is gone. Keep the split on the record even though the file no longer needs it: it is why the classification question is asked per SCHEMA rather than per file, and a file-level verdict here would have been wrong in one direction or the other whichever way it fell — either tightening a dead slot or leaving the live anonymous-access door open | +`theme.zod.ts` had this ledger's longest row (authorable, strict as of #4001 批 15, site count 14 → 6 after #5021's retirements) until **#10485 retired the file whole** (ADR-0049 enforce-or-remove; maintainer ruling 2026-08-21, disposition B: 退役授权面). The row's two measured doors — `stack.zod.ts`'s `themes: z.array(ThemeSchema)` and `defineTheme()` — were the whole pipeline: authored themes were parsed and ingested and read by NOTHING downstream, so the carrier was strict-deleted with a `guidance` prescription (`app.branding` is the one colour surface) and every schema in the file left with it, six retired-def entries plus the D3 semantic entry `stack-themes-carrier-retired` recording the move. The row's history — the per-block passthrough split, the measured alias curation, finding 7's three signposts — lives on in the git history of this file and in the tombstone record on #5021/#3494; what this ledger keeps is the same lesson `notification.zod.ts`'s paragraph above records: strictness made the dropped keys loud, and it took ADR-0049 — a different question — to notice the whole surface was a door to nowhere. + `notification.zod.ts` had a row here (`authorable (p)`, resolved to **`no door`** at #4001 批 14) until #5015 retired `NotificationActionSchema` under ADR-0049 enforce-or-remove. The file survives and still exports its three presentation enums (`NotificationType` / `NotificationSeverity` / `NotificationPosition`, which objectui's toaster reads as a vocabulary) — but those are `z.enum`s, so the file now has **zero object sites** and nothing left for this ledger to classify. #4610 had already dropped two sites from it by deleting the `Notification` / `NotificationConfig` wrappers for having zero consumers; removing the action shape they would have carried is the end of that same thread. Worth keeping the trail: the row's value was never its site count but its demonstration that *having a consumer is not having an authoring door* — objectui read `NotificationActionSchema.shape.variant` as a vocabulary the whole time the shape was unreachable and unparsed. **批 13 的五行 triage 行已在 #4988 删除,去向记在这里** — `animation.zod.ts` / From 846b572aa3eeface0a1458af5931a97859e04585 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 08:51:28 +0000 Subject: [PATCH 4/5] chore: regenerate spec artifacts on merged tree (post-merge lap, #10485) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw --- content/docs/references/index.mdx | 9 +- content/docs/references/ui/index.mdx | 1 - content/docs/references/ui/meta.json | 1 - content/docs/references/ui/theme.mdx | 144 ------------------ ...07-unknown-key-strictness-ledger.counts.md | 13 +- packages/spec/api-surface-signatures.json | 1 - packages/spec/api-surface/root.json | 1 - packages/spec/api-surface/ui.json | 14 -- packages/spec/authorable-defaults/ui.json | 1 - packages/spec/authorable-surface/ui.json | 57 ------- packages/spec/export-origins/root.json | 1 - packages/spec/export-origins/ui.json | 14 -- packages/spec/json-schema.manifest/ui.json | 6 - .../meta-spelling/meta-url-data.generated.ts | 1 - 14 files changed, 10 insertions(+), 254 deletions(-) delete mode 100644 content/docs/references/ui/theme.mdx diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 3dc3f1d836..0241c62d32 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1589 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1583 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -32,8 +32,8 @@ counts are sums of the rows they head. Regenerate with | [Shared Protocol](/docs/references/shared) | 8 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. | | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 36 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | -| [UI Protocol](/docs/references/ui) | 17 | 162 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **199** | **1589** | 14 protocol modules | +| [UI Protocol](/docs/references/ui) | 16 | 156 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | +| **Total** | **198** | **1583** | 14 protocol modules | --- @@ -364,7 +364,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a ## UI Protocol -**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **17 pages, 162 schemas** +**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 156 schemas** Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. @@ -385,7 +385,6 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay | [`report.zod.ts`](/docs/references/ui/report) | `JoinedReportBlock`, `Report`, `ReportChart`, `ReportSort`, `ReportType` | | [`responsive.zod.ts`](/docs/references/ui/responsive) | `BreakpointColumnMap`, `BreakpointName`, `BreakpointOrderMap`, `ResponsiveConfig`, `ResponsiveStyles`, `StyleMap` | | [`sharing.zod.ts`](/docs/references/ui/sharing) | `SharingConfig` | -| [`theme.zod.ts`](/docs/references/ui/theme) | `BorderRadius`, `ColorPalette`, `Shadow`, `Theme`, `ThemeMode`, `Typography` | | [`view.zod.ts`](/docs/references/ui/view) | `AddRecordConfig`, `AppearanceConfig`, `CalendarConfig`, `ColumnPrefix`, `ColumnSummary`, `ColumnSummaryConfig`, `FormButtonConfig`, `FormField`, `FormFieldPublicPicker`, `FormSection`, `FormView`, `GalleryConfig`, `GanttConfig`, `GanttQuickFilter`, `GroupingConfig`, `GroupingField`, `HttpMethodSubset`, `HttpRequest`, `KanbanConfig`, `ListChartConfig`, `ListColumn`, `ListMapConfig`, `ListView`, `NavigationConfig`, `NavigationMode`, `ObjectListView`, `ObjectUserFilters`, `PaginationConfig`, `RowColorConfig`, `RowHeight`, `SelectionConfig`, `TimelineConfig`, `TreeConfig`, `UserActionsConfig`, `UserFilterField`, `UserFilters`, `View`, `ViewData`, `ViewFilterRule`, `ViewItem`, `ViewItemName`, `ViewItemWire`, `ViewKind`, `ViewScope`, `ViewSharing`, `ViewTab`, `VisualizationType` | --- diff --git a/content/docs/references/ui/index.mdx b/content/docs/references/ui/index.mdx index 19cb0ebd07..7fee054ec5 100644 --- a/content/docs/references/ui/index.mdx +++ b/content/docs/references/ui/index.mdx @@ -21,6 +21,5 @@ This section contains all protocol schemas for the ui layer of ObjectStack. - diff --git a/content/docs/references/ui/meta.json b/content/docs/references/ui/meta.json index 5eedbea53a..761d57bf10 100644 --- a/content/docs/references/ui/meta.json +++ b/content/docs/references/ui/meta.json @@ -16,7 +16,6 @@ "report", "---Interaction & Layout---", "responsive", - "theme", "---Platform---", "i18n", "notification", diff --git a/content/docs/references/ui/theme.mdx b/content/docs/references/ui/theme.mdx deleted file mode 100644 index d8e46c344d..0000000000 --- a/content/docs/references/ui/theme.mdx +++ /dev/null @@ -1,144 +0,0 @@ ---- -title: Theme -description: Theme protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -Color Palette Schema -Defines brand colors and their variants. - - -**Source:** `packages/spec/src/ui/theme.zod.ts` - - -## TypeScript Usage - -```typescript -import { BorderRadiusSchema, ColorPaletteSchema, ShadowSchema, ThemeSchema, ThemeModeSchema, TypographySchema } from '@objectstack/spec/ui'; -import type { BorderRadius, ColorPalette, Shadow, Theme, ThemeMode, Typography } from '@objectstack/spec/ui'; - -// Validate data -const result = BorderRadiusSchema.parse(data); -``` - ---- - -## BorderRadius - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **none** | `string` | optional | No border radius (0) | -| **sm** | `string` | optional | Small border radius (e.g., 0.125rem) | -| **base** | `string` | optional | Base border radius (e.g., 0.25rem) | -| **md** | `string` | optional | Medium border radius (e.g., 0.375rem) | -| **lg** | `string` | optional | Large border radius (e.g., 0.5rem) | -| **xl** | `string` | optional | Extra large border radius (e.g., 0.75rem) | -| **2xl** | `string` | optional | 2X large border radius (e.g., 1rem) | -| **full** | `string` | optional | Full border radius (50%) | - - ---- - -## ColorPalette - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **primary** | `string` | ✅ | Primary brand color (hex, rgb, or hsl) | -| **secondary** | `string` | optional | Secondary brand color | -| **accent** | `string` | optional | Accent color for highlights | -| **success** | `string` | optional | Success state color (default: green) | -| **warning** | `string` | optional | Warning state color (default: yellow) | -| **error** | `string` | optional | Error state color (default: red) | -| **info** | `string` | optional | Info state color (default: blue) | -| **background** | `string` | optional | Background color | -| **surface** | `string` | optional | Surface/card background color | -| **text** | `string` | optional | Primary text color | -| **textSecondary** | `string` | optional | Secondary text color | -| **border** | `string` | optional | Border color | -| **disabled** | `string` | optional | Disabled state color | -| **primaryLight** | `string` | optional | Lighter shade of primary | -| **primaryDark** | `string` | optional | Darker shade of primary | -| **secondaryLight** | `string` | optional | Lighter shade of secondary | -| **secondaryDark** | `string` | optional | Darker shade of secondary | - - ---- - -## Shadow - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **none** | `string` | optional | No shadow | -| **sm** | `string` | optional | Small shadow | -| **base** | `string` | optional | Base shadow | -| **md** | `string` | optional | Medium shadow | -| **lg** | `string` | optional | Large shadow | -| **xl** | `string` | optional | Extra large shadow | -| **2xl** | `string` | optional | 2X large shadow | -| **inner** | `string` | optional | Inner shadow (inset) | - - ---- - -## Theme - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Unique theme identifier (snake_case) | -| **label** | `string` | ✅ | Human-readable theme name | -| **description** | `string` | optional | Theme description | -| **mode** | `Enum<'light' \| 'dark' \| 'auto'>` | optional (default: `"light"`) | Theme mode (light, dark, or auto) | -| **colors** | `{ primary: string; secondary?: string; accent?: string; success?: string; … }` | ✅ | Color palette configuration | -| **typography** | `{ fontFamily?: object }` | optional | Typography settings | -| **borderRadius** | `{ none?: string; sm?: string; base?: string; md?: string; … }` | optional | Border radius scale | -| **shadows** | `{ none?: string; sm?: string; base?: string; md?: string; … }` | optional | Box shadow effects | -| **animation** | `never` | optional | [REMOVED] `theme.animation` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — unlike the #3494 props above, the engine DID emit `--duration-*` and `--timing-*`, faithfully and for years; what never existed was a reader. No first-party component or stylesheet has ever consumed one, so every transition ran at the renderer default whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "duration-fast": "150ms", "timing-ease_in": "cubic-bezier(0.4, 0, 1, 1)" }` emits exactly the same properties). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **zIndex** | `never` | optional | [REMOVED] `theme.zIndex` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--z-base` … `--z-tooltip` and nothing read one, so an overlay you "lifted" still stacked by document order. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "z-modal": "1050" }` emits exactly the same `--z-modal`). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **customVars** | `Record` | optional | Custom CSS variables (key-value pairs) | -| **extends** | `string` | optional | Base theme to extend from | -| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | -| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | -| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | -| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). | -| **_packageId** | `string` | optional | Owning package machine id. | -| **_packageVersion** | `string` | optional | Owning package version. | -| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | - - ---- - -## ThemeMode - -### Allowed Values - -* `light` -* `dark` -* `auto` - - ---- - -## Typography - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **fontFamily** | `{ base?: string }` | optional | | -| **fontSize** | `never` | optional | [REMOVED] `theme.typography.fontSize` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-size-xs` … `--font-size-4xl` faithfully and NO first-party component or stylesheet has ever read one, so a declared type scale was real CSS that styled nothing. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-size-lg": "1.125rem" }` emits exactly the same `--font-size-lg`). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **fontWeight** | `never` | optional | [REMOVED] `theme.typography.fontWeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-weight-*` and nothing read it, so text rendered at the inherited weight whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-weight-semibold": "600" }` emits exactly the same `--font-weight-semibold`). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **lineHeight** | `never` | optional | [REMOVED] `theme.typography.lineHeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--line-height-*` and nothing read it, so every block kept its inherited leading. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "line-height-relaxed": "1.75" }` emits exactly the same `--line-height-relaxed`). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **letterSpacing** | `never` | optional | [REMOVED] `theme.typography.letterSpacing` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--letter-spacing-*` and nothing read it, so tracking never moved. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "letter-spacing-wide": "0.025em" }` emits exactly the same `--letter-spacing-wide`). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | - - ---- - diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index 6b8938713c..fa26c70714 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -21,7 +21,7 @@ regenerate. | Measure | Value | |---|---| | Triaged directories | 5 | -| Object sites in them | 445 | +| Object sites in them | 439 | | Still-open (strip) sites | 124 | | Files carrying at least one | 22 | @@ -44,12 +44,12 @@ The `strict` column is the one the campaign schedules against; it counts both th | Dir | Sites | strict | passthrough | catchall | strip | |---|---|---|---|---|---| -| `ui/` | 177 | 165 | 5 | 0 | 7 | +| `ui/` | 171 | 159 | 5 | 0 | 7 | | `data/` | 156 | 74 | 1 | 0 | 81 | | `automation/` | 65 | 42 | 0 | 0 | 23 | | `security/` | 20 | 7 | 0 | 0 | 13 | | `studio/` | 27 | 27 | 0 | 0 | 0 | -| **total** | **445** | **315** | **6** | **0** | **124** | +| **total** | **439** | **309** | **6** | **0** | **124** | ## File-level triage — site counts @@ -74,10 +74,9 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `report.zod.ts` | 3 | | `responsive.zod.ts` | 4 | | `sharing.zod.ts` | 1 | -| `theme.zod.ts` | 6 | | `view.zod.ts` | 57 | | `widget.zod.ts` | 1 | -| **total** | **177** | +| **total** | **171** | ### `data/` — sites @@ -156,7 +155,7 @@ over it is here. ### `ui/` — open -**7 strip of 177**, in 4 file(s). +**7 strip of 171**, in 4 file(s). | File | Strip | Sites | |---|---|---| @@ -164,7 +163,7 @@ over it is here. | `app.zod.ts` | 1 | 18 | | `view.zod.ts` | 4 | 57 | | `widget.zod.ts` | 1 | 1 | -| **total** | **7** | **177** | +| **total** | **7** | **171** | | Bucket | Sites | |---|---| diff --git a/packages/spec/api-surface-signatures.json b/packages/spec/api-surface-signatures.json index 61cbb5119e..b2099d1182 100644 --- a/packages/spec/api-surface-signatures.json +++ b/packages/spec/api-surface-signatures.json @@ -21,7 +21,6 @@ "defineSharingRule": "sha256:239e6649b55712a4", "defineSkill": "sha256:b928cee8cb3c2861", "defineStack": "sha256:4d36d9603c011c44", - "defineTheme": "sha256:21563e61ca2209e8", "defineTool": "sha256:47ab5254a14f1cf5", "defineTranslationBundle": "sha256:2716798bbd575af2", "defineView": "sha256:00cf9d0d2f44e843", diff --git a/packages/spec/api-surface/root.json b/packages/spec/api-surface/root.json index a32100f87b..3d0bbf5610 100644 --- a/packages/spec/api-surface/root.json +++ b/packages/spec/api-surface/root.json @@ -155,7 +155,6 @@ "defineSharingRule (function)", "defineSkill (function)", "defineStack (function)", - "defineTheme (function)", "defineTool (function)", "defineTranslationBundle (function)", "defineView (function)", diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index 972e7560f4..73a48b4051 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -47,8 +47,6 @@ "AssembledViewArtifactParsed (type)", "AssembledViewArtifactSchema (const)", "AssembledViewPartition (interface)", - "BorderRadius (type)", - "BorderRadiusSchema (const)", "BreakpointColumnMap (type)", "BreakpointColumnMapSchema (const)", "BreakpointName (type)", @@ -92,8 +90,6 @@ "ChartSeriesSchema (const)", "ChartType (type)", "ChartTypeSchema (const)", - "ColorPalette (type)", - "ColorPaletteSchema (const)", "ColumnPrefix (type)", "ColumnPrefixParsed (type)", "ColumnPrefixSchema (const)", @@ -331,25 +327,16 @@ "SelectionConfig (type)", "SelectionConfigParsed (type)", "SelectionConfigSchema (const)", - "Shadow (type)", - "ShadowSchema (const)", "SharingConfig (type)", "SharingConfigParsed (type)", "SharingConfigSchema (const)", "StyleMap (type)", "StyleMapSchema (const)", - "Theme (type)", - "ThemeMode (type)", - "ThemeModeSchema (const)", - "ThemeParsed (type)", - "ThemeSchema (const)", "TimelineConfig (type)", "TimelineConfigParsed (type)", "TimelineConfigSchema (const)", "TreeConfig (type)", "TreeConfigSchema (const)", - "Typography (type)", - "TypographySchema (const)", "UrlNavItem (type)", "UrlNavItemParsed (type)", "UrlNavItemSchema (const)", @@ -420,7 +407,6 @@ "defineForm (function)", "definePage (function)", "defineReport (function)", - "defineTheme (function)", "defineView (function)", "defineViewItem (function)", "diagnoseViewMetadata (function)", diff --git a/packages/spec/authorable-defaults/ui.json b/packages/spec/authorable-defaults/ui.json index 3945f9b364..6a7edd4d6b 100644 --- a/packages/spec/authorable-defaults/ui.json +++ b/packages/spec/authorable-defaults/ui.json @@ -105,7 +105,6 @@ "ui/SelectionConfig:type = \"none\"", "ui/SharingConfig:allowAnonymous = false", "ui/SharingConfig:enabled = false", - "ui/Theme:mode = \"light\"", "ui/TimelineConfig:scale = \"week\"", "ui/UrlNavItem:target = \"_self\"", "ui/UserActionsConfig:addRecordForm = false", diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index 8333f9d866..57d99fd848 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -139,14 +139,6 @@ "ui/AriaProps:ariaDescribedBy", "ui/AriaProps:ariaLabel", "ui/AriaProps:role", - "ui/BorderRadius:2xl", - "ui/BorderRadius:base", - "ui/BorderRadius:full", - "ui/BorderRadius:lg", - "ui/BorderRadius:md", - "ui/BorderRadius:none", - "ui/BorderRadius:sm", - "ui/BorderRadius:xl", "ui/BreakpointColumnMap:2xl", "ui/BreakpointColumnMap:lg", "ui/BreakpointColumnMap:md", @@ -238,23 +230,6 @@ "ui/ChartSeries:type", "ui/ChartSeries:variant", "ui/ChartSeries:yAxis", - "ui/ColorPalette:accent", - "ui/ColorPalette:background", - "ui/ColorPalette:border", - "ui/ColorPalette:disabled", - "ui/ColorPalette:error", - "ui/ColorPalette:info", - "ui/ColorPalette:primary", - "ui/ColorPalette:primaryDark", - "ui/ColorPalette:primaryLight", - "ui/ColorPalette:secondary", - "ui/ColorPalette:secondaryDark", - "ui/ColorPalette:secondaryLight", - "ui/ColorPalette:success", - "ui/ColorPalette:surface", - "ui/ColorPalette:text", - "ui/ColorPalette:textSecondary", - "ui/ColorPalette:warning", "ui/ColumnPrefix:field", "ui/ColumnPrefix:type", "ui/ColumnSummaryConfig:field", @@ -1118,39 +1093,12 @@ "ui/RowColorConfig:colors", "ui/RowColorConfig:field", "ui/SelectionConfig:type", - "ui/Shadow:2xl", - "ui/Shadow:base", - "ui/Shadow:inner", - "ui/Shadow:lg", - "ui/Shadow:md", - "ui/Shadow:none", - "ui/Shadow:sm", - "ui/Shadow:xl", "ui/SharingConfig:allowAnonymous", "ui/SharingConfig:allowedDomains", "ui/SharingConfig:enabled", "ui/SharingConfig:expiresAt", "ui/SharingConfig:password", "ui/SharingConfig:publicLink", - "ui/Theme:_lock", - "ui/Theme:_lockDocsUrl", - "ui/Theme:_lockReason", - "ui/Theme:_lockSource", - "ui/Theme:_packageId", - "ui/Theme:_packageVersion", - "ui/Theme:_provenance", - "ui/Theme:animation [RETIRED]", - "ui/Theme:borderRadius", - "ui/Theme:colors", - "ui/Theme:customVars", - "ui/Theme:description", - "ui/Theme:extends", - "ui/Theme:label", - "ui/Theme:mode", - "ui/Theme:name", - "ui/Theme:shadows", - "ui/Theme:typography", - "ui/Theme:zIndex [RETIRED]", "ui/TimelineConfig:colorField", "ui/TimelineConfig:endDateField", "ui/TimelineConfig:groupByField", @@ -1161,11 +1109,6 @@ "ui/TreeConfig:fields", "ui/TreeConfig:labelField", "ui/TreeConfig:parentField", - "ui/Typography:fontFamily", - "ui/Typography:fontSize [RETIRED]", - "ui/Typography:fontWeight [RETIRED]", - "ui/Typography:letterSpacing [RETIRED]", - "ui/Typography:lineHeight [RETIRED]", "ui/UrlNavItem:badge", "ui/UrlNavItem:badgeVariant", "ui/UrlNavItem:icon", diff --git a/packages/spec/export-origins/root.json b/packages/spec/export-origins/root.json index f256024404..6699a2fd4a 100644 --- a/packages/spec/export-origins/root.json +++ b/packages/spec/export-origins/root.json @@ -155,7 +155,6 @@ "defineSharingRule": "src/security/sharing.zod.ts#defineSharingRule (function)", "defineSkill": "src/ai/skill.zod.ts#defineSkill (function)", "defineStack": "src/stack.zod.ts#defineStack (function)", - "defineTheme": "src/ui/theme.zod.ts#defineTheme (function)", "defineTool": "src/ai/tool.zod.ts#defineTool (function)", "defineTranslationBundle": "src/system/translation.zod.ts#defineTranslationBundle (function)", "defineView": "src/ui/view.zod.ts#defineView (function)", diff --git a/packages/spec/export-origins/ui.json b/packages/spec/export-origins/ui.json index 23c74a8f86..df6d7b3150 100644 --- a/packages/spec/export-origins/ui.json +++ b/packages/spec/export-origins/ui.json @@ -47,8 +47,6 @@ "AssembledViewArtifactParsed": "src/ui/assembled-views.zod.ts#AssembledViewArtifactParsed (type)", "AssembledViewArtifactSchema": "src/ui/assembled-views.zod.ts#AssembledViewArtifactSchema (const)", "AssembledViewPartition": "src/ui/assembled-views.zod.ts#AssembledViewPartition (interface)", - "BorderRadius": "src/ui/theme.zod.ts#BorderRadius (type)", - "BorderRadiusSchema": "src/ui/theme.zod.ts#BorderRadiusSchema (const)", "BreakpointColumnMap": "src/ui/responsive.zod.ts#BreakpointColumnMap (type)", "BreakpointColumnMapSchema": "src/ui/responsive.zod.ts#BreakpointColumnMapSchema (const)", "BreakpointName": "src/ui/responsive.zod.ts#BreakpointName (type)", @@ -92,8 +90,6 @@ "ChartSeriesSchema": "src/ui/chart.zod.ts#ChartSeriesSchema (const)", "ChartType": "src/ui/chart.zod.ts#ChartType (type)", "ChartTypeSchema": "src/ui/chart.zod.ts#ChartTypeSchema (const)", - "ColorPalette": "src/ui/theme.zod.ts#ColorPalette (type)", - "ColorPaletteSchema": "src/ui/theme.zod.ts#ColorPaletteSchema (const)", "ColumnPrefix": "src/ui/view.zod.ts#ColumnPrefix (type)", "ColumnPrefixParsed": "src/ui/view.zod.ts#ColumnPrefixParsed (type)", "ColumnPrefixSchema": "src/ui/view.zod.ts#ColumnPrefixSchema (const)", @@ -331,25 +327,16 @@ "SelectionConfig": "src/ui/view.zod.ts#SelectionConfig (type)", "SelectionConfigParsed": "src/ui/view.zod.ts#SelectionConfigParsed (type)", "SelectionConfigSchema": "src/ui/view.zod.ts#SelectionConfigSchema (const)", - "Shadow": "src/ui/theme.zod.ts#Shadow (type)", - "ShadowSchema": "src/ui/theme.zod.ts#ShadowSchema (const)", "SharingConfig": "src/ui/sharing.zod.ts#SharingConfig (type)", "SharingConfigParsed": "src/ui/sharing.zod.ts#SharingConfigParsed (type)", "SharingConfigSchema": "src/ui/sharing.zod.ts#SharingConfigSchema (const)", "StyleMap": "src/ui/responsive.zod.ts#StyleMap (type)", "StyleMapSchema": "src/ui/responsive.zod.ts#StyleMapSchema (const)", - "Theme": "src/ui/theme.zod.ts#Theme (type)", - "ThemeMode": "src/ui/theme.zod.ts#ThemeMode (type)", - "ThemeModeSchema": "src/ui/theme.zod.ts#ThemeModeSchema (const)", - "ThemeParsed": "src/ui/theme.zod.ts#ThemeParsed (type)", - "ThemeSchema": "src/ui/theme.zod.ts#ThemeSchema (const)", "TimelineConfig": "src/ui/view.zod.ts#TimelineConfig (type)", "TimelineConfigParsed": "src/ui/view.zod.ts#TimelineConfigParsed (type)", "TimelineConfigSchema": "src/ui/view.zod.ts#TimelineConfigSchema (const)", "TreeConfig": "src/ui/view.zod.ts#TreeConfig (type)", "TreeConfigSchema": "src/ui/view.zod.ts#TreeConfigSchema (const)", - "Typography": "src/ui/theme.zod.ts#Typography (type)", - "TypographySchema": "src/ui/theme.zod.ts#TypographySchema (const)", "UrlNavItem": "src/ui/app.zod.ts#UrlNavItem (type)", "UrlNavItemParsed": "src/ui/app.zod.ts#UrlNavItemParsed (type)", "UrlNavItemSchema": "src/ui/app.zod.ts#UrlNavItemSchema (const)", @@ -420,7 +407,6 @@ "defineForm": "src/ui/view.zod.ts#defineForm (function)", "definePage": "src/ui/page.zod.ts#definePage (function)", "defineReport": "src/ui/report.zod.ts#defineReport (function)", - "defineTheme": "src/ui/theme.zod.ts#defineTheme (function)", "defineView": "src/ui/view.zod.ts#defineView (function)", "defineViewItem": "src/ui/view.zod.ts#defineViewItem (function)", "diagnoseViewMetadata": "src/ui/view.zod.ts#diagnoseViewMetadata (function)", diff --git a/packages/spec/json-schema.manifest/ui.json b/packages/spec/json-schema.manifest/ui.json index eb2b124e4e..6174a2db03 100644 --- a/packages/spec/json-schema.manifest/ui.json +++ b/packages/spec/json-schema.manifest/ui.json @@ -16,7 +16,6 @@ "ui/AppContextSelector", "ui/AppearanceConfig", "ui/AriaProps", - "ui/BorderRadius", "ui/BreakpointColumnMap", "ui/BreakpointName", "ui/BreakpointOrderMap", @@ -35,7 +34,6 @@ "ui/ChartInteraction", "ui/ChartSeries", "ui/ChartType", - "ui/ColorPalette", "ui/ColumnPrefix", "ui/ColumnSummary", "ui/ColumnSummaryConfig", @@ -139,14 +137,10 @@ "ui/RowColorConfig", "ui/RowHeight", "ui/SelectionConfig", - "ui/Shadow", "ui/SharingConfig", "ui/StyleMap", - "ui/Theme", - "ui/ThemeMode", "ui/TimelineConfig", "ui/TreeConfig", - "ui/Typography", "ui/UrlNavItem", "ui/UserActionsConfig", "ui/UserFilterField", diff --git a/packages/spec/src/meta-spelling/meta-url-data.generated.ts b/packages/spec/src/meta-spelling/meta-url-data.generated.ts index c237b1a091..e87a0ec394 100644 --- a/packages/spec/src/meta-spelling/meta-url-data.generated.ts +++ b/packages/spec/src/meta-spelling/meta-url-data.generated.ts @@ -24,7 +24,6 @@ export const META_URL_TO_SINGULAR: Readonly> = Object.fre "reports": "report", "datasets": "dataset", "actions": "action", - "themes": "theme", "flows": "flow", "jobs": "job", "positions": "position", From 90dc771f2593e1d32d73397e587b95371230ba13 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 13:09:33 +0000 Subject: [PATCH 5/5] docs(quick-reference): drop the retired Theme row and recount the UI section (10 of 16) (#10485) check:quick-reference-counts [row]+[total] remedies: references/ui/theme.mdx was deleted with the retired surface, so the row goes (retired, not moved) and M drops 17 -> 16, N 11 -> 10. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw --- content/docs/getting-started/quick-reference.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/content/docs/getting-started/quick-reference.mdx b/content/docs/getting-started/quick-reference.mdx index d4873fe836..93397ba6e3 100644 --- a/content/docs/getting-started/quick-reference.mdx +++ b/content/docs/getting-started/quick-reference.mdx @@ -46,7 +46,7 @@ Core business logic and data modeling schemas. | **[Postgres Driver](/docs/references/data/driver-postgres)** | `driver/postgres.zod.ts` | PostgresConfig | PostgreSQL configuration | | **[Mongo Driver](/docs/references/data/driver-mongo)** | `driver/mongo.zod.ts` | MongoConfig | MongoDB configuration | -## UI Protocol (11 of 17 schemas) +## UI Protocol (10 of 16 schemas) Presentation layer - views, forms, dashboards, and app branding. @@ -61,7 +61,6 @@ Presentation layer - views, forms, dashboards, and app branding. | **[Action](/docs/references/ui/action)** | `action.zod.ts` | Action, ActionType | UI button actions | | **[Component](/docs/references/ui/component)** | `component.zod.ts` | PageComponent variants | Reusable UI components | | **[Chart](/docs/references/ui/chart)** | `chart.zod.ts` | Chart, ChartType | Chart definitions | -| **[Theme](/docs/references/ui/theme)** | `theme.zod.ts` | Theme, ColorPalette | Theming and branding | | **[Widget Contract](/docs/protocol/objectui/widget-contract)** ↗ | `widget.zod.ts` | FieldWidgetProps | Props a custom field widget receives — the contract is documented with ObjectUI, outside `references/ui/` | ## Kernel Protocol (17 of 31 schemas)